Java并发编程从入门到精通 - 第3章:Thread安全

Posted kehuaihan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程从入门到精通 - 第3章:Thread安全相关的知识,希望对你有一定的参考价值。

Java内存模型与多线程:

线程不安全与线程安全:
 线程安全问题阐述:
  多条语句操作多个线程共享的资源时,一个线程只执行了部分语句,还没执行完,另一个线程又进来操作共享数据(执行语句),导致共享数据最终结果出现误差;所以就是看一个线程能否每次在没有其他线程进入的情况下操作完包含共享资源的语句块,如果能就没有安全问题,不能就有安全问题;
 如何模拟多线程的安全问题:
  用Thread.sleep()方法模拟; 放在哪:放在多线程操作共享数据的语句块之间(使正在运行的线程休息一会,让其他线程执行,就会出现共享数据错误的问题);
 如何解决线程安全问题:
  解决思想:
   只有当一个线程执行完所有语句之后,才能让另外一个线程进来再进行操作;
  具体操作:
   加锁,对操作共享数据的代码块加锁,实现在一个线程操作共享数据时,其它线程不能再进来操作,直到本线程执行完之后其它线程才能进来执行;
  哪些代码块需要加锁(同步):
   明确每个线程都会操作的代码块;
   明确共享资源;
   明确代码块中操作共享资源语句块,这些语句块就是需要加锁的代码块;
  具体解决方式:(以synchronized为例)
   同步代码块:
    synchronized(对象)
    {
     需要被同步的代码块;
    }
   同步方法:
    就是把需要同步的代码块放到一个函数里面,代码块原来所在的函数里面可能还有其他不需要同步的代码块(所以不能每次直接同步原来所在的方法),需要仔细分析;
 确保没有线程安全问题的两个前提:
  至少有两个及两个以上的线程操作共享资源;
  所有线程使用的锁是同一个锁;
  注意:加了锁之后还出现线程安全问题的话,说明上面两个前提肯定没有全部满足;
 想实现线程安全大致有三种方法:
  多实例,也就是不使用单例模式了(单例模式在多线程下是不安全的);
  使用java.util.concurrent下面的类库;
  使用锁机制synchronized、lock方式;

 1 /**
 2  * 线程安全实例:(加锁的情况下)假设5个用户,都来给一个数字加1的工作,那么最后应该是得到加5的结果
 3  */
 4 package thread02;
 5 
 6 public class ThreadSafeTest01
 7 {
 8     public static void main(String[] args)
 9     {
10         Count2 count = new Count2();
11         
12         for(int i=0;i<5;i++)
13         {
14             Person2 person = new Person2(count);
15             Thread thread = new Thread(person);
16             thread.start();
17         }
18         
19         try
20         {
21             Thread.sleep(1000);
22         } 
23         catch (InterruptedException e)
24         {
25             e.printStackTrace();
26         }
27         
28         System.out.println("程序结束,num最后的值:" + count.getNum());
29     }
30 }
31 
32 class Count2
33 {
34     private int num = 0;
35     
36     // synchronized必须放在返回值类型前面
37     public synchronized void add()
38     {
39         try
40         {
41             Thread.sleep(50);
42         } 
43         catch (InterruptedException e)
44         {
45             e.printStackTrace();
46         }
47         
48         num += 1;
49         
50         System.out.println(Thread.currentThread().getName() + ":" + num);
51     }
52     
53     public int getNum()
54     {
55         return num;
56     }
57 }
58 
59 class Person2 implements Runnable
60 {
61     private Count2 count;
62     
63     public Person2(Count2 count)
64     {
65         this.count = count;
66     }
67     
68     @Override
69     public void run()
70     {
71         count.add();
72     }
73 }
74 
75 /*
76 最终结果:
77 Thread-0:1
78 Thread-4:2
79 Thread-3:3
80 Thread-2:4
81 Thread-1:5
82 程序结束,num最后的值:5
83 
84 根据结果可以看出:线程安全;线程按进入顺序主次将num值加1,最后结果始终为5;
85 
86 */
线程安全实例:(加锁的情况下)假设5个用户,都来给一个数字加1的工作,那么最后应该是得到加5的结果
 1 /**
 2  * 线程不安全实例:(不加锁的情况下)假设5个用户,都来给一个数字加1的工作,那么最后应该是得到加5的结果
 3  */
 4 package thread02;
 5 
 6 public class ThreadUnSafeTest01
 7 {
 8     public static void main(String[] args)
 9     {
10         Count count = new Count();
11         
12         for(int i=0;i<5;i++)
13         {
14             Person person = new Person(count);
15             Thread thread = new Thread(person);
16             thread.start();
17         }
18         
19         try
20         {
21             Thread.sleep(1000);
22         } 
23         catch (InterruptedException e)
24         {
25             e.printStackTrace();
26         }
27         
28         System.out.println("程序执行结束,num最后结果:" + count.getNum());
29     }
30 }
31 
32 class Count
33 {
34     private int num = 0;
35     
36     public void add()
37     {
38         try
39         {
40             // 模拟做事
41             Thread.sleep(50);
42         } 
43         catch (InterruptedException e)
44         {
45             e.printStackTrace();
46         }
47         
48         num += 1;
49         
50         System.out.println(Thread.currentThread().getName() + ":" + num);
51     }
52 
53     public int getNum()
54     {
55         return num;
56     }
57     public void setNum(int num)
58     {
59         this.num = num;
60     }
61 }
62 
63 class Person implements Runnable
64 {
65     private Count count;
66     
67     public Person(Count count)
68     {
69         this.count = count;
70     }
71     
72     @Override
73     public void run()
74     {
75         count.add();
76     }
77 }
78 
79 /*
80 最后结果:
81 Thread-1:2
82 Thread-4:4
83 Thread-3:4
84 Thread-2:2
85 Thread-0:2
86 程序执行结束,num最后结果:4
87 
88 根据结果可以看出:线程不安全,并不是每个线程按顺序将num按序加1;
89 
90 */
线程不安全实例:(不加锁的情况下)假设5个用户,都来给一个数字加1的工作,那么最后应该是得到加5的结果

为什么单例在多线程下是不安全的:
 因为在多线程下可能会创建多个实例,不能保证原子性,违背设计单例模式的初衷;

synchronized:
 详解:
  隐式锁,同步锁,内置锁,监视器锁,可重入锁;
  为了解决线程同步问题而生;
  当用它来修饰一个代码块或一个方法时,能够保证在同一时刻最多只有一个线程执行该段代码(或方法);
  采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁(对象,锁必须是对象,就是引用类型,不能是基本数据类型)叫做互斥锁;每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池(谁进入锁池);对于任何一个对象,系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作;每个对象的锁只能分配给一个线程,因此叫做互斥锁;
  是可重入锁:一个线程可以多次获得同一个对象的互斥锁;
 使用同步机制获取互斥锁的规则说明:
  如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝;(不解)
  类的每个实例都有自己的对象级别锁(一个实例对象就是一个互斥锁,同一个类的两个实例对象对应的互斥锁是不一样的);当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁(就是当前对象的意思);
  持有一个对象级别锁不会阻止该线程被交换出来(不解),也不会阻塞其他线程访问同一实例对象中的非synchronized代码;
  持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外;
  使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁;
  类级别锁被特定类的所有实例共享,它用于控制对static成员变量以及static方法的并发访问;具体用法与对象级别锁相似;
 synchronized的不同写法对于性能和执行效率的优劣程度排序:
  同步方法体 < 同步方法块(锁不是最小的锁) < 同步方法块(锁是最小的锁);

  1 /**
  2  * 一个正确的例子:虽然synchronized的写法不一样,但下面的这两个方法对于多线程来说是线程安全的;
  3  * 因为满足两个前提:多个线程(至少两个)线程操作同一个共享资源;多个线程使用的锁是同一个锁(本例中是当前对象);
  4  */
  5 package thread02;
  6 
  7 public class SynchronizedTest01
  8 {
  9     public static void main(String[] args)
 10     {
 11         Count1 count = new Count1();
 12         
 13         for(int i=1;i<=6;i++)
 14         {
 15             Person1 person = new Person1(count);
 16             Thread thread = new Thread(person);
 17             
 18             if(i%2 == 0)
 19                 person.setFlag(false);
 20             else
 21                 person.setFlag(true);
 22             
 23             thread.start();
 24         }
 25     }
 26 }
 27 
 28 class Count1
 29 {
 30     private int sum = 0;
 31     
 32     // 使用的锁是当前对象
 33     public synchronized void add()
 34     {
 35         try
 36         {
 37             Thread.sleep(50);
 38         } 
 39         catch (InterruptedException e)
 40         {
 41             e.printStackTrace();
 42         }
 43         
 44         sum += 1;
 45         System.out.println(Thread.currentThread().getName() + " -> add() -> " + sum);
 46     }
 47     
 48     public void add2()
 49     {
 50         // 使用的锁是当前对象
 51         synchronized (this)
 52         {
 53             try
 54             {
 55                 Thread.sleep(50);
 56             } 
 57             catch (InterruptedException e)
 58             {
 59                 e.printStackTrace();
 60             }
 61             
 62             sum += 1;
 63             System.out.println(Thread.currentThread().getName() + " -> add2() -> " + sum);
 64         }
 65     }
 66 }
 67 
 68 class Person1 implements Runnable
 69 {
 70     private Count1 count;
 71     boolean flag = false;
 72     
 73     public Person1(Count1 count)
 74     {
 75         this.count = count;
 76     }
 77 
 78     @Override
 79     public void run()
 80     {
 81         if(flag)
 82             count.add();
 83         else
 84             count.add2();
 85     }
 86 
 87     public boolean isFlag()
 88     {
 89         return flag;
 90     }
 91     public void setFlag(boolean flag)
 92     {
 93         this.flag = flag;
 94     }
 95     
 96 }
 97 
 98 /*
 99 最终结果:
100 Thread-0 -> add() -> 1
101 Thread-5 -> add2() -> 2
102 Thread-4 -> add() -> 3
103 Thread-3 -> add2() -> 4
104 Thread-2 -> add() -> 5
105 Thread-1 -> add2() -> 6
106 
107 根据结果:程序是线程安全的,因为满足线程安全的两个前提
108 */
一个正确的例子:虽然synchronized的写法不一样,但下面的这两个方法对于多线程来说是线程安全的;
  1 /**
  2  * 一个正确的例子:下面的这两个方法对于多线程来说是线程不安全的;
  3  * 因为违背了线程安全的两个前提中的一个:多个线程使用同一个锁;及本例两个方法使用的锁是不一样的
  4  */
  5 package thread02;
  6 
  7 public class SynchronizedTest02
  8 {
  9     public static void main(String[] args)
 10     {
 11         Count3 count = new Count3();
 12         
 13         for(int i=1;i<=6;i++)
 14         {
 15             Person3 person = new Person3(count);
 16             Thread thread = new Thread(person);
 17             
 18             if(i%2 ==0)
 19                 person.setFlag(false);
 20             else
 21                 person.setFlag(true);
 22             
 23             thread.start();
 24         }
 25     }
 26 }
 27 
 28 class Count3
 29 {
 30     private int sum = 0;
 31     private byte[] bt = new byte[1];
 32     
 33     // 使用的锁是当前对象this
 34     public synchronized void add()
 35     {
 36         try
 37         {
 38             Thread.sleep(50);
 39         } 
 40         catch (InterruptedException e)
 41         {
 42             e.printStackTrace();
 43         }
 44         
 45         sum += 1;
 46         System.out.println(Thread.currentThread().getName() + " -> add() -> " + sum);
 47     }
 48     
 49     public void add2()
 50     {
 51         // 使用的锁不是当前对象this,而是自定义的对象bt
 52         synchronized (bt)
 53         {
 54             try
 55             {
 56                 Thread.sleep(50);
 57             } 
 58             catch (InterruptedException e)
 59             {
 60                 e.printStackTrace();
 61             }
 62             
 63             sum += 1;
 64             System.out.println(Thread.currentThread().getName() + " -> add() -> " + sum);
 65         }
 66     }
 67 }
 68 
 69 class Person3 implements Runnable
 70 {
 71     private Count3 count;
 72     private boolean flag = false;
 73     
 74     public Person3(Count3 count)
 75     {
 76         this.count = count;
 77     }
 78 
 79     @Override
 80     public void run()
 81     {
 82         if(flag)
 83             count.add();
 84         else
 85             count.add2();
 86     }
 87     
 88     public void setFlag(boolean flag)
 89     {
 90         this.flag = flag;
 91     }
 92 }
 93 
 94 /*
 95 最终结果:
 96 Thread-1 -> add() -> 2
 97 Thread-0 -> add() -> 2
 98 Thread-5 -> add() -> 3
 99 Thread-4 -> add() -> 4
100 Thread-3 -> add() -> 5
101 Thread-2 -> add() -> 6
102 
103 根据结果:程序是线程不安全的,因为多个线程没有使用同一个锁
104 */
一个正确的例子:下面的这两个方法对于多线程来说是线程不安全的;
  1 /**
  2  * 实例证明synchronized的不同写法对于性能和执行效率的优劣程度排序:
  3  *   同步方法体 < 同步方法块(锁不是最小的锁) < 同步方法块(锁是最小的锁);
  4  */
  5 package thread02;
  6 
  7 public class SynchronizedTest03
  8 {
  9     public static void main(String[] args)
 10     {
 11         Count4 count = new Count4();
 12         Thread thread = new Thread(count);
 13         thread.start();
 14     }
 15 }
 16 
 17 class Count4 implements Runnable
 18 {
 19     private int sum = 0;
 20     private byte[] bt = new byte[1];
 21     
 22     public synchronized void add()
 23     {
 24         try
 25         {
 26             Thread.sleep(1000);
<

以上是关于Java并发编程从入门到精通 - 第3章:Thread安全的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程从入门到精通 - 第2章:认识Thread

Java并发编程从入门到精通 - 第5章:多线程之间的交互:线程阀

Java并发编程从入门到精通 - 第7章:Fork/Join框架

Java并发编程从入门到精通-总纲

Java并发编程从入门到精通 张振华.Jack --我的书

java必看书籍