Java多线程笔记

来源:互联网 发布:软件停止工作怎么办 编辑:程序博客网 时间:2024/06/09 18:24

多线程优点

  • 资源利用率更好(在发生IO等待时,利用处理器做其他事情)
  • 程序设计在某些情况下更简单
  • 程序响应更快

      

    多线程代价

  • 设计复杂(线程交互复杂,错误难以发现,重现和修复)
  • 上下文切换开销
  • 增加资源消耗(需要内存维持线程的本地堆栈)

      

    创建和运行Java线程

  • 创建Thread的子类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public class MyThread extends Thread {

    public void run(){

         System.out.println("MyThread running");

    }

}

MyThread myThread = new MyThread();

myTread.start();

  

Thread thread = new Thread(){

   public void run(){

     System.out.println("Thread Running");

   }

};

thread.start();

  

  • 实现Runable接口

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class MyRunnable implements Runnable {

    public void run(){

        System.out.println("MyRunnable running");

    }

}

Thread thread = new Thread(new MyRunnable());

thread.start();

  

Runnable myRunnable = new Runnable(){

   public void run(){

     System.out.println("Runnable running");

   }

}

Thread thread = new Thread(myRunnable);

thread.start();

  

  • 线程名

1

2

3

4

MyRunnable runnable = new MyRunnable();

Thread thread = new Thread(runnable, "New Thread");

thread.start();

System.out.println(thread.getName());

  

线程安全和共享资源

  • 局部变量
    局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。
  • 局部的对象引用
    对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。 如果在某个方法中创建的对象不会逃逸出(即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。 实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。
  • 对象成员
    对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。
  • 线程控制逃逸规则
    1.如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
    2.即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。 比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。

      

      

    线程安全及不可变性

  • 创建不可变的共享对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

publicclass ImmutableValue{

    privateint value = 0;         //没有set方法

  

    public ImmutableValue(int value){

        this.value = value;

    }

  

    publicint getValue(){

        returnthis.value;

    }

      

    /*

    **构造新对象返回

    */

    public ImmutableValue add(int valueToAdd){

        returnnew ImmutableValue(this.value + valueToAdd);

    }

}

  

  • ImmutableValue类是线程安全的,但使用它的类未必安全,如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

publicvoid Calculator{

    private ImmutableValue currentValue = null;

  

    public ImmutableValue getValue(){

        return currentValue;

    }

  

    publicvoid setValue(ImmutableValue newValue){

        this.currentValue = newValue;

    }

  

    publicvoid add(int newValue){

        this.currentValue = this.currentValue.add(newValue);

    }

}

要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可

  

Java同步块

  • 实例方法同步(锁定实例对象,一个实例一个线程)

1

2

3

publicsynchronizedvoid add(int value){

    this.count += value;

}

  

  • 静态方法同步(锁定Class对象,一个类一个线程)

1

2

3

publicstaticsynchronizedvoid add(int value){

    count += value;

}

  

  • 实例方法中的同步块(锁定this对象,一个this对象一个线程)

1

2

3

4

5

publicvoid add(int value){

    synchronized(this){

       this.count += value;

    }

}

  

  • 静态方法中的同步块(锁定Class对象,一个类一个线程)

1

2

3

4

5

6

7

8

publicclass MyClass {

    publicstaticvoid log2(String msg1, String msg2){

       synchronized(MyClass.class){

          log.writeln(msg1);

          log.writeln(msg2);

       }

    }

}

  

  

线程通信

  • 通过共享对象通信

1

2

3

4

5

6

7

8

9

10

11

12

publicclass MySignal{

  

    protectedboolean hasDataToProcess = false;

  

    publicsynchronizedboolean hasDataToProcess(){

        returnthis.hasDataToProcess;

    }

  

    publicsynchronizedvoid setHasDataToProcess(boolean hasData){

        this.hasDataToProcess = hasData;

    }

}

  

  • 忙等待

1

2

3

4

5

6

7

protected MySignal sharedSignal = ...

  

...

  

while(!sharedSignal.hasDataToProcess()){

  //do nothing... busy waiting

}

  

  • wait(),notify()和notifyAll()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

publicclass MonitorObject{

}

  

publicclass MyWaitNotify{

  

    MonitorObject myMonitorObject = new MonitorObject();

  

    publicvoid doWait(){

      synchronized(myMonitorObject){

        try{

          myMonitorObject.wait();

        } catch(InterruptedException e){...}

      }

    }

  

    publicvoid doNotify(){

      synchronized(myMonitorObject){

        myMonitorObject.notify();

      }

    }

}

  

  • 丢失的信号

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

publicclass MyWaitNotify2{

  

  MonitorObject myMonitorObject = new MonitorObject();

  boolean wasSignalled = false;

  

  publicvoid doWait(){

    synchronized(myMonitorObject){

      if(!wasSignalled){          //防止notify之后才wait,再也无法醒来

        try{

          myMonitorObject.wait();

         } catch(InterruptedException e){...}

      }

      //clear signal and continue running.

      wasSignalled = false;

    }

  }

  

  publicvoid doNotify(){

    synchronized(myMonitorObject){

      wasSignalled = true;

      myMonitorObject.notify();

    }

  }

}

  

  • 假唤醒

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

publicclass MyWaitNotify3{

  

  MonitorObject myMonitorObject = new MonitorObject();

  boolean wasSignalled = false;

  

  publicvoid doWait(){

    synchronized(myMonitorObject){

      while(!wasSignalled){          //如果发生假唤醒,wasSignalledfalse,将进入循环再次睡眠

        try{

          myMonitorObject.wait();

         } catch(InterruptedException e){...}

      }

      //clear signal and continue running.

      wasSignalled = false;

    }

  }

  

  publicvoid doNotify(){

    synchronized(myMonitorObject){

      wasSignalled = true;

      myMonitorObject.notify();

    }

  }

}

  

  • 多个线程等待相同信号 如果你有多个线程在等待,被notifyAll()唤醒,但只有一个被允许继续执行,使用while循环也是个好方法。 每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出wait()调用并清除wasSignalled标志(设为false)。 一旦这个线程退出doWait()的同步块,其他线程退出wait()调用,并在while循环里检查wasSignalled变量值。 但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。

      

  • 不要在字符串常量或全局对象中调用wait()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

publicclass MyWaitNotify{

  

  String myMonitorObject = "";

  boolean wasSignalled = false;

  

  publicvoid doWait(){

    synchronized(myMonitorObject){

      while(!wasSignalled){

        try{

          myMonitorObject.wait();

         } catch(InterruptedException e){...}

      }

      //clear signal and continue running.

      wasSignalled = false;

    }

  }

  

  publicvoid doNotify(){

    synchronized(myMonitorObject){

      wasSignalled = true;

      myMonitorObject.notify();

    }

  }

}

在空字符串作为锁的同步块(或者其他常量字符串)里调用wait()和notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。 这意味着,即使你有2个不同的MyWaitNotify实例,它们都引用了相同的空字符串实例。 同时也意味着存在这样的风险:在第一个MyWaitNotify实例上调用doWait()的线程会被在第二个MyWaitNotify实例上调用doNotify()的线程唤醒。 

  

  

死锁

简单的死锁

Thread 1 locks A, waits for B

Thread 2 locks B, waits for A

  

更复杂的死锁

Thread 1 locks A, waits for B

Thread 2 locks B, waits for C

Thread 3 locks C, waits for D

Thread 4 locks D, waits for A

  

数据库的死锁

Transaction 1, request 1, locks record 1 for update

Transaction 2, request 1, locks record 2 for update

Transaction 1, request 2, tries to lock record 2 for update.

Transaction 2, request 2, tries to lock record 1 for update.

  

避免死锁

  • 加锁顺序
  • 加锁时限
  • 死锁检测
  • 加锁顺序(按照相同的顺序获得锁)

    Thread 1

    lock A

    lock B

    Thread 2

    wait for A

    lock C (when A locked)

    Thread 3

    wait for A

    wait for B

    wait for C

  • 加锁时限

    Thread 1 locks A

    Thread 2 locks B

    Thread 1 attempts to lock B but is blocked

    Thread 2 attempts to lock A but is blocked

    Thread 1's lock attempt on B times out

    Thread 1 backs up and releases A as well

    Thread 1 waits randomly (e.g. 257 millis) before retrying.

    Thread 2's lock attempt on A times out

    Thread 2 backs up and releases B as well

    Thread 2 waits randomly (e.g. 43 millis) before retrying.

    如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)

  • 死锁检测

    每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

    当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。

    下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。

    当检测出死锁时:

    一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁

    一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级

      

    饥饿和公平

    1、Java中导致饥饿的原因:

  • 高优先级线程吞噬所有低优先级线程的CPU时间(线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台。对大多数应用来说,你最好是不要改变其优先级值)
  • 线程被永久堵塞在一个等待进入同步块的状态(运气太差,总是得不到运行机会)
  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法,运气太差,总是没有被唤醒)

      

    2、在Java中实现公平性方案,需要:

  • 使用锁,而不是同步块
  • 公平锁
  • 注意性能方面

    公平锁代码实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

publicclass FairLock {

    privateboolean isLocked = false;

    private Thread lockingThread = null;

    private List<QueueObject> waitingThreads =

            new ArrayList<QueueObject>();

  

    publicvoid lock() throws InterruptedException{

        QueueObject queueObject           = new QueueObject();

        boolean     isLockedForThisThread = true;

        synchronized(this){

            waitingThreads.add(queueObject);

        }

  

        while(isLockedForThisThread){

          synchronized(this){

            isLockedForThisThread =

                isLocked || waitingThreads.get(0) != queueObject;

            if(!isLockedForThisThread){

              isLocked = true;

               waitingThreads.remove(queueObject);

               lockingThread = Thread.currentThread();

               return;

             }

          }

          try{

            queueObject.doWait();

          }catch(InterruptedException e){

            synchronized(this) { waitingThreads.remove(queueObject); }

            throw e;

          }

        }

    }

  

    publicsynchronizedvoid unlock(){

      if(this.lockingThread != Thread.currentThread()){

        thrownew IllegalMonitorStateException(

          "Calling thread has not locked this lock");

      }

      isLocked      = false;

      lockingThread = null;

      if(waitingThreads.size() > 0){

        waitingThreads.get(0).doNotify();

      }

    }

}

  

  

/*****************************************************/

publicclass QueueObject {

  

    privateboolean isNotified = false;

  

    publicsynchronizedvoid doWait() throws InterruptedException {

  

        while(!isNotified){

            this.wait();

        }

  

        this.isNotified = false;

  

    }

  

    publicsynchronizedvoid doNotify() {

        this.isNotified = true;

        this.notify();

    }

  

    publicboolean equals(Object o) {

        returnthis == o;

    }

}

FairLock新创建了一个QueueObject的实例,并对每个调用lock()的线程进行入队列。调用unlock()的线程将从队列头部获取QueueObject,并对其调用doNotify(),以唤醒在该对象上等待的线程。通过这种方式,在同一时间仅有一个等待线程获得唤醒,而不是所有的等待线程。这也是实现FairLock公平性的核心所在。

还需注意到,QueueObject实际是一个semaphore。doWait()和doNotify()方法在QueueObject中保存着信号。这样做以避免一个线程在调用queueObject.doWait()之前被另一个调用unlock()并随之调用queueObject.doNotify()的线程重入,从而导致信号丢失。queueObject.doWait()调用放置在synchronized(this)块之外,以避免被monitor嵌套锁死,所以另外的线程可以解锁,只要当没有线程在lock方法的synchronized(this)块中执行即可。

最后,注意到queueObject.doWait()在try – catch块中是怎样调用的。在InterruptedException抛出的情况下,线程得以离开lock(),并需让它从队列中移除。

  

  

0 0
原创粉丝点击