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 */
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 */
为什么单例在多线程下是不安全的:
因为在多线程下可能会创建多个实例,不能保证原子性,违背设计单例模式的初衷;
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 */
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); 27 } 28 catch (InterruptedException e) 29 { 30 e.printStackTrace(); 31 } 32 33 sum += 1; 34 System.out.println("sum:" + sum); 35 } 36 37 public void add2() 38 { 39 long beginTime = System.currentTimeMillis(); 40 41 synchronized (this) 42 { 43 try 44 { 45 Thread.sleep(1000); 46 } 47 catch (InterruptedException e) 48 { 49 e.printStackTrace(); 50 } 51 52 sum += 1; 53 System.out.println("sum:" + sum); 54 } 55 56 long endTime = System.currentTimeMillis(); 57 long result = endTime - beginTime; 58 System.out.println("使用同步方法块(锁不是最小的锁)耗时:" + result); 59 } 60 61 public void add3() 62 { 63 long beginTime = System.currentTimeMillis(); 64 65 synchronized (bt) 66 { 67 try 68 { 69 Thread.sleep(1000); 70 } 71 catch (InterruptedException e) 72 { 73 e.printStackTrace(); 74 } 75 76 sum += 1; 77 System.out.println("sum:" + sum); 78 } 79 80 long endTime = System.currentTimeMillis(); 81 long result = endTime - beginTime; 82 System.out.println("使用同步方法块(锁是最小的锁)耗时:" + result); 83 } 84 85 @Override 86 public void run() 87 { 88 long beginTime1 = System.currentTimeMillis(); 89 add(); 90 long endTime1 = System.currentTimeMillis(); 91 long result1 = endTime1 - beginTime1; 92 System.out.println("使用同步方法体耗时:" + result1); 93 94 long beginTime2 = System.currentTimeMillis(); 95 add2(); 96 long endTime2 = System.currentTimeMillis(); 97 long result2 = endTime2 - beginTime2; 98 System.out.println("使用同步方法块(锁不是最小的锁)耗时:" + result2); 99 100 long beginTime3 = System.currentTimeMillis(); 101 add3(); 102 long endTime3 = System.currentTimeMillis(); 103 long result3 = endTime3 - beginTime3; 104 System.out.println("使用同步方法块(锁是最小的锁)耗时:" + result3); 105 } 106 } 107 108 /* 109 最终结果: 110 sum:1 111 使用同步方法体耗时:1011 112 sum:2 113 使用同步方法块(锁不是最小的锁)耗时:1007 114 使用同步方法块(锁不是最小的锁)耗时:1007 115 sum:3 116 使用同步方法块(锁是最小的锁)耗时:1001 117 使用同步方法块(锁是最小的锁)耗时:1001 118 119 根据结果:同步方法体 < 同步方法块(锁不是最小的锁) < 同步方法块(锁是最小的锁); 120 */
显式锁:
详解:
为什么叫显式锁,因为所有加锁和解锁的方法都是显示的,即必须手动加锁和释放锁;
为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在try语句块内,并在finally语句块中释放锁;尤其当有return语句时,return语句必须放在try字句中,以确保unlock()不会过早发生,从而将数据暴露给第二个任务;
采用lock加锁和释放锁的一般形式如下:
接口:Lock ReadWriteLock
实现类:ReentrantLock ReentrantReadWriteLock
ReentrantLock是Lock接口的实现类, ReentrantReadWriteLock是ReadWriteLock接口的实现类;
ReadWriteLock并不是Lock的子接口,只是ReadWriteLock借助Lock来实现读写两个锁的并存、互斥的机制;
Lock:是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显示的;
ReentrantLock:在竞争条件下,ReentrantLock的实现要比现在的synchronized实现更具有可伸缩性;(有可能在JVM的将来版本中改进synchronized的竞争性能)这意味着当许多线程都竞争相同锁定时,使用ReentrantLock的吞吐量通常要比synchronized好;换句话说,当许多线程试图访问ReentrantLock保护的共享资源时,JVM将花费较少的时间来调度线程,而用更多时间执行线程;ReentrantLock是在工作中对方法块加锁使用频率最高的;但ReentrantLock也有一个主要缺点:它可能忘记释放锁定;
1 /** 2 * ReentrantLock的使用规范 3 */ 4 package thread02; 5 6 import java.util.concurrent.locks.ReentrantLock; 7 8 public class ReentrantLockTest01 9 { 10 public static void main(String[] args) 11 { 12 Count5 count = new Count5(); 13 14 new Thread() 15 { 16 public void run() 17 { 18 count.get(); 19 }; 20 }.start(); 21 } 22 } 23 24 class Count5 25 { 26 // ReentrantLock的使用规范:在try..catch..外先加锁;在try..catch..中执行业务逻辑;在finally中释放锁; 27 // 只是建议,具体情况具体分析 28 29 final ReentrantLock lock = new ReentrantLock(); 30 31 public void get() 32 { 33 // ReentrantLock只适用于方法块,不适用于方法;即只能在方法块上加锁,不能在方法上加锁 34 lock.lock(); // 1.加锁 35 try 36 { 37 // 2.执行相关的业务逻辑 38 System.out.println(Thread.currentThread().getName() + " get begin..."); 39 Thread.sleep(1000L); 40 System.out.println(Thread.currentThread().getName() + " get end..."); 41 } 42 catch (InterruptedException e) 43 { 44 e.printStackTrace(); 45 } 46 finally 47 { 48 // 3.解锁 49 lock.unlock(); 50 } 51 } 52 }
1 /** 2 * 实例比较证明:因为ReentrantLock只适用于方法块,所以每个线程在方法块中新定义ReentrantLock锁时,这些锁都是相互独立的,不是互斥的, 3 * 在这种情况下是线程不安全的;而synchronized对象之间是互斥关系,只要使用synchronized锁住方法块,那么每个线程必须按顺序进入方法块 4 * 执行任务,且每次只能有一个线程进入方法块执行任务,这种情况下是线程安全的; 5 * 得出一个结论:要想线程安全,ReentrantLock锁的定义要放到方法的外面,在方法内进行加锁解锁操作;这样多线程访问方法中的方法块时,使用的 6 * 锁就是同一个锁,这样就能实现线程安全; 7 */ 8 package thread02; 9 10 import java.util.concurrent.locks.ReentrantLock; 11 12 public class ReentrantLockTest02 13 { 14 public static void main(String[] args) 15 { 16 Count6 count = new Count6(); 17 18 // 以下四个线程使用的是同一个共享资源:count 19 for(int i=0;i<2;i++) 20 { 21 new Thread() 22 { 23 public void run() 24 { 25 count.get(); 26 }; 27 }.start(); 28 } 29 30 for(int i=0;i<2;i++) 31 { 32 new Thread() 33 { 34 public void run() 35 { 36 count.put(); 37 }; 38 }.start(); 39 } 40 } 41 } 42 43 class Count6 44 { 45 // ReentrantLock锁在方法外定义,在多线程情况下,是安全的 46 // final ReentrantLock lock = new ReentrantLock(); 47 48 public void get() 49 { 50 // 方法里面的变量只能加final或不加任何限制符;因为在方法中,是局部变量,只能在方法中使用,作用域已经限定死了,不需要再加访问限定符; 51 // ReentrantLock锁在方法内定义,在多线程情况下,是不安全的 52 final ReentrantLock lock = new ReentrantLock(); 53 54 lock.lock(); 55 try 56 { 57 System.out.println(Thread.currentThread().getName() + " get begin..."); 58 Thread.sleep(1000L); 59 System.out.println(Thread.currentThread().getName() + " get end..."); 60 } 61 catch (InterruptedException e) 62 { 63 e.printStackTrace(); 64 } 65 finally 66 { 67 lock.unlock(); 68 } 69 } 70 71 public void put() 72 { 73 synchronized (this) 74 { 75 try 76 { 77 System.out.println(Thread.currentThread().getName() + " put begin..."); 78 Thread.sleep(1000L); 79 System.out.println(Thread.currentThread().getName() + " put end..."); 80 } 81 catch (InterruptedException e) 82 { 83 e.printStackTrace(); 84 } 85 } 86 } 87 } 88 89 /* 90 最终结果:每次可能不一样 91 Thread-1 get begin... 92 Thread-0 get begin... 93 Thread-2 put begin... 94 Thread-1 get end... 95 Thread-2 put end... 96 Thread-3 put begin... 97 Thread-0 get end... 98 Thread-3 put end... 99 100 结果证明:ReentrantLock锁在方法内定义,在多线程情况下,是不安全的;synchronized (this)是安全的 101 */
1 /** 2 * 实例证明:以下两个方法之间的锁是独立的;多线程场景下是不安全的 3 */ 4 package thread02; 5 6 import java.util.concurrent.locks.ReentrantLock; 7 8 public class ReentrantLockTest03 9 { 10 public static void main(String[] args) 11 { 12 Count7 count = new Count7(); 13 14 for(int i=0;i<2;i++) 15 { 16 new Thread() 17 { 18 public void run() 19 { 20 count.get(); 21 }; 22 }.start(); 23 } 24 25 for(int i=0;i<2;i++) 26 { 27 new Thread() 28 { 29 public void run() 30 { 31 count.put(); 32 }; 33 }.start(); 34 } 35 } 36 } 37 38 class Count7 39 { 40 public void get() 41 { 42 // ReentrantLock锁定义在方法内,每次一个线程访问这个方法都要新建一个锁,而这些新建的锁都是相互独立了,不是互斥的,互不影响 43 final ReentrantLock lock = new ReentrantLock(); 44 lock.lock(); 45 try 46 { 47 System.out.println(Thread.currentThread().getName() + " get begin..."); 48 Thread.sleep(1000L); 49 System.out.println(Thread.currentThread().getName() + " get end..."); 50 } 51 catch (InterruptedException e) 52 { 53 e.printStackTrace(); 54 } 55 finally 56 { 57 lock.unlock(); 58 } 59 } 60 61 public void put() 62 { 63 final ReentrantLock lock = new ReentrantLock(); 64 lock.lock(); 65 try 66 { 67 System.out.println(Thread.currentThread().getName() + " put begin..."); 68 Thread.sleep(1000L); 69 System.out.println(Thread.currentThread().getName() + " put end..."); 70 } 71 catch (InterruptedException e) 72 { 73 e.printStackTrace(); 74 } 75 finally 76 { 77 lock.unlock(); 78 } 79 } 80 } 81 82 /* 83 最终结果: 84 Thread-0 get begin... 85 Thread-2 put begin... 86 Thread-1 get begin... 87 Thread-3 put begin... 88 Thread-1 get end... 89 Thread-3 put end... 90 Thread-0 get end... 91 Thread-2 put end... 92 93 结果证明:这样写法是线程不安全的 94 */
1 /** 2 * 实例证明:以下两个方法之间的锁是互斥的;多线程场景下是安全的 3 */ 4 package thread02; 5 6 import java.util.concurrent.locks.ReentrantLock; 7 8 public class ReentrantLockTest04 9 { 10 public static void main(String[] args) 11 { 12 Count8 count = new Count8(); 13 14 for(int i=0;i<2;i++) 15 { 16 new Thread() 17 { 18 public void run() 19 { 20 count.get(); 21 }; 22 }.start(); 23 } 24 25 for(int i=0;i<2;i++) 26 { 27 new Thread() 28 { 29 public void run() 30 { 31 count.put(); 32 }; 33 }.start(); 34 } 35 } 36 } 37 38 class Count8 39 { 40 // ReentrantLock锁定义在方法之外,所有方法都使用这个锁,所以是线程安全的 41 private ReentrantLock lock = new ReentrantLock(); 42 43 public void get() 44 { 45 lock.lock(); 46 try 47 { 48 System.out.println(Thread.currentThread().getName() + " get begin..."); 49 Thread.sleep(1000L); 50 System.out.println(Thread.currentThread().getName() + " get end..."); 51 } 52 catch (InterruptedException e) 53 { 54 e.printStackTrace(); 55 } 56 finally 57 { 58 lock.unlock(); 59 } 60 } 61 62 public void put() 63 { 64 lock.lock(); 65 try 66 { 67 System.out.println(Thread.currentThread().getName() + " put begin..."); 68 Thread.sleep(1000L); 69 System.out.println(Thread.currentThread().getName() + " put end..."); 70 } 71 catch (InterruptedException e) 72 { 73 e.printStackTrace(); 74 } 75 finally 76 { 77 lock.unlock(); 78 } 79 } 80 } 81 82 /* 83 最终结果: 84 Thread-0 get begin... 85 Thread-0 get end... 86 Thread-1 get begin... 87 Thread-1 get end... 88 Thread-3 put begin... 89 Thread-3 put end... 90 Thread-2 put begin... 91 Thread-2 put end... 92 93 结果证明:这样写是线程安全的 94 */
Lock和synchronized的比较:
Lock使用起来比较灵活,但是必须有释放锁的动作配合;
Lock必须手动释放和开启锁,而synchronized不需要手动释放和开启锁;
Lock只适用于代码块锁,而synchronized对象之间是互斥关系;
ReadWriteLock接口:
提供了readLock和writeLock两种锁的操作机制;
一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程;也就是说读写锁适用的场合是一个共享资源被大量读取操作,而只有少量的写操作;
在ReadWriteLock中,每次读取共享数据就需要读取锁,当需要修改共享数据时就需要写入锁;看起来好像是两个锁,其实不是;
ReentrantReadWriteLock类:
ReadWriteLock接口唯一的实现类;
主要应用场景是:当有很多线程都从某个数据结构读取数据,而很少有线程对其进行修改时,在这种情况下,允许读取器线程共享访问是合适的,写入器线程依然必须是互斥访问的;
主要特性:
公平性:
重入性:
锁降级:
锁升级:
锁获取中断:
条件变量:
重入数:
以上概括起来就是读写锁的机制:读-读不互斥、读-写互斥、写-写互斥;
1 /** 2 * 读写锁 ReentrantReadWriteLock 使用规范; 3 * 只是建议,具体情况具体分析 4 */ 5 package thread02; 6 7 import java.util.concurrent.locks.Lock; 8 import java.util.concurrent.locks.ReentrantReadWriteLock; 9 10 public class ReentrantReadWriteLockTest01 11 { 12 public static void main(String[] args) 13 { 14 Count9 count = new Count9(); 15 16 for(int i=0;i<2;i++) 17 { 18 new Thread() 19 { 20 public void run() 21 { 22 count.get(); 23 }; 24 }.start(); 25 } 26 27 for(int i=0;i<2;i++) 28 { 29 new Thread() 30 { 31 public void run() 32 { 33 count.put(); 34 }; 35 }.start(); 36 } 37 } 38 } 39 40 class Count9 41 { 42 // 1.创建一个 ReentrantReadWriteLock 对象 43 private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 44 // 2.得到一个可被多个读操作共用的读锁(会排斥所有写操作) 45 private Lock readLock = rwl.readLock(); 46 // 3.得到一个写锁(会排斥所有其它读操作和写操作) 47 private Lock writeLock = rwl.writeLock(); 48 49 public void get() 50 { 51 // 4.在try..catch..外,实现业务逻辑前,加(读写)锁 52 readLock.lock(); 53 try 54 { 55 // 5.在try..catch..中实现具体的业务逻辑 56 System.out.println(Thread.currentThread().getName() + " read begin..."); 57 Thread.sleep(1000L); 58 System.out.println(Thread.currentThread().getName() + " read end..."); 59 } 60 catch (InterruptedException e) 61 { 62 e.printStackTrace(); 63 } 64 finally 65 { 66 // 6.在finally中释放锁 67 readLock.unlock(); 68 } 69 } 70 71 public void put() 72 { 73 writeLock.lock(); 74 try 75 { 76 System.out.println(Thread.currentThread().getName() + " write begin..."); 77 Thread.sleep(1000L); 78 System.out.println(Thread.currentThread().getName() + " write end..."); 79 } 80 catch (InterruptedException e) 81 { 82 e.printStackTrace(); 83 } 84 finally 85 { 86 writeLock.unlock(); 87 } 88 } 89 } 90 91 /* 92 最终结果: 93 Thread-0 read begin... 94 Thread-1 read begin... 95 Thread-0 read end... 96 Thread-1 read end... 97 Thread-2 write begin... 98 Thread-2 write end... 99 Thread-3 write begin... 100 Thread-3 write end... 101 102 根据结果:读-读不互斥、写-写互斥 103 */
1 /** 2 * 实例:ReadLock(读锁)和 WriteLock(写锁)单独使用的场景 3 */ 4 package thread02; 5 6 import java.util.concurrent.locks.ReentrantReadWriteLock; 7 8 public class ReentrantReadWriteLockTest02 9 { 10 public static void main(String[] args) 11 { 12 // 注意final的效果,可加可不加 13 final Count10 count = new Count10(); 14 15 for(int i=0;i<2;i++) 16 { 17 new Thread() 18 { 19 public void run() 20 { 21 count.get(); 22 }; 23 }.start(); 24 } 25 26 for(int i=0;i<2;i++) 27 { 28 new Thread() 29 { 30 public void run() 31 { 32 count.put(); 33 }; 34 }.start(); 35 } 36 } 37 } 38 39 class Count10 40 { 41 // 注意final的效果,可加可不加 42 private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 43 44 public void get() 45 { 46 // 上读锁,其它线程只能读不能写,具有高并发性 47 rwl.readLock().lock(); 48 try 49 { 50 System.out.println(Thread.currentThread().getName() + " read begin..."); 51 Thread.sleep(1000L); // 模拟干活 52 System.out.println(Thread.currentThread().getName() + " read end..."); 53 } 54 catch (InterruptedException e) 55 { 56 e.printStackTrace(); 57 } 58 finally 59 { 60 // 释放毒素哦,最好放在finally中 61 rwl.readLock().unlock(); 62 } 63 } 64 65 public void put() 66 { 67 // 上写锁,具有阻塞性 68 rwl.writeLock().lock(); 69 try 70 { 71 System.out.println(Thread.currentThread().getName() + " write begin..."); 72 Thread.sleep(1000L); // 模拟干活 73 System.out.println(Thread.currentThread().getName() + " write end..."); 74 } 75 catch (InterruptedException e) 76 { 77 e.printStackTrace(); 78 } 79 finally 80 { 81 // 释放写锁,最好放在finally中 82 rwl.writeLock().unlock(); 83 } 84 } 85 }
1 /** 2 * 实例:ReadLock(读锁)和 WriteLock(写锁)的复杂使用场景,实现一个缓存系统 3 * 缓存系统: 4 * 在程序运行过程中,有些数据我们不会经常修改,例如数据库中性别字段,但是我们却经常使用,如果每次都从数据库中获取,那么将会降低程序性能; 5 * 那么我们可以在内存中分配一个区域专门存放我们第一次从数据库中拿出的数据;思路如下:我们使用Map来充当我们的缓存区域,当使用性别值时, 6 * 可以先看看map中是否有值,如果有那么拿出来,如果没有那么查询数据库,并为map赋值; 7 * 8 * 思路如上,但是我们要考虑多线程的问题,如果多个程序同时使用该缓存系统,有的读,有的写,那么很有可能在某一时刻,一个线程为map赋值了, 9 * 但是另一个线程或多个线程会重复为map赋值;这就是多线程的并发问题,如何解决,我们采用Java5的读写锁来实现; 10 * ReentrantReadWriteLock:读锁和写锁同步,读的时候不允许写,写的时候不允许读,可以同时读; 11 */ 12 package thread02; 13 14 import java.util.HashMap; 15 import java.util.Map; 16 import java.util.concurrent.locks.ReentrantReadWriteLock; 17 18 public class ReentrantReadWriteLockTest03 19 { 20 // 构造缓存对象(模拟存放数据的缓存) 21 private final Map<String, Object> catchData = new HashMap<String, Object>(); 22 // 构造读写锁 23 private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 24 25 public static void main(String[] args) 26 { 27 ReentrantReadWriteLockTest03 rwlt = new ReentrantReadWriteLockTest03(); 28 System.out.println(rwlt.getData("1")); 29 } 30 31 public Object getData(String key) 32 { 33 Object value = null; 34 35 try 36 { 37 rwl.readLock().lock(); // 当线程开始读时,首先开始加上读锁 38 39 value = catchData.get(key); // 从缓存中获取值 40 if(null == value) // 判断是否存在值 41 { 42 // 缓存中不存在这个值,则从数据库中获取 43 try 44 { 45 rwl.readLock().unlock(); // 在开始写之前,首先要释放写锁,否则写锁无法拿到(将读取数据库的返回值赋值给value就是写操作) 46 rwl.writeLock().lock(); // 获取写锁开始写数据 47 48 // 这里再次判断该值是否为空;因为如果两个写线程如果都阻塞在里,当一个线程被唤醒后value的值不为null(进行了赋值操作), 49 // 当另外一个线程也被唤醒,如果不判断就会再次进行赋值操作,也就是最终进行了两次写操作,可能导致最后获取的数据不一致(因为两次查询数据库的返回值不一定一样) 50 if(null == value) 51 { 52 value = "从数据库读取出来的值"; // 将数据库返回值赋值给value,就是进行了写操作 53 catchData.put(key, value); // 将数据放到缓存中,这样下次直接在缓存中就可以获取到值 54 } 55 56 rwl.readLock().lock(); // 写完之后重入降级为读锁 57 // 读线程释放锁,写线程加锁; 58 // 第一个写线程开始执行任务:读取数据库数据(值为1)赋值给value,最后读线程读取并返回的value值为1; 59 // 第一个写线程任务执行完毕,释放锁; 60 // 问题来了:第一个写线程释放锁后,可能第二个写线程加锁; 61 // 第二个写线程开始执行任务,读取数据库数据(值为2)赋值给value,value的在返回之前又被改变了; 62 // 第二个写线程任务执行完毕,释放锁; 63 // 读线程加锁,读取数据,返回的value值为2; 64 } 65 finally 66 { 67 rwl.writeLock().unlock(); // 最后释放写锁 68 } 69 70 } 71 else 72 { 73 System.out.println("从缓存中读取到了数据:" + value); 74 } 75 } 76 finally 77 { 78 rwl.readLock().unlock(); // 释放读锁 79 } 80 return value; 81 } 82 }
ReentrantReadWriteLock与ReentrantLock的比较:
相同点:都是显式锁,需要手动加锁解锁,都很适合高并发场景;
不同点:
ReentrantReadWriteLock是对ReentrantLock的复杂扩展,能适合更加复杂的业务;
ReentrantReadWriteLock能实现一个方法中读写分离的锁的机制,而ReentrantLock加锁解锁只有一种机制;
显式锁(Lock)和隐式锁(synchronized)的比较:
ReentrantLock主要增了如下的高级功能:
1、等待可中断:
当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情,它对处理执行时间上的同步块很有帮助;而在等待由synchronized产生的互斥锁时,会一直阻塞,是不能被中断的;
2、可实现公平锁:
多个线程在等待同一个锁时,必须按照申请锁的时间顺序排队等待;而非公平锁则不保证这点,在锁释放时,任何一个等待锁的线程都有机会获得锁; synchronized中的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(ture)来要求使用公平锁;
3、锁可以绑定多个条件:
ReentrantLock对象可以同时绑定多个Condition对象(名曰:条件变量或条件队列);而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁;而ReentrantLock则无需这么做,只需要多次调用newCondition()方法即可;而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其他线程);
其他方面的比较:
synchronized:读写互斥、写写互斥、读读互斥(读读操作不会引发安全问题);ReentrantReadWriteLock(读写锁):读写互斥、写写互斥、读写不互斥;
悲观锁 与 乐观锁:
悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁;传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁;
乐观锁:
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制;乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁;
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量;但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适;
显示锁StampedLock:
该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写;
读不阻塞写的实现思路:
在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!
因为在读线程非常多而写线程比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,因此写线程可能几乎很少被调度成功!当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。所以读写都存在的情况下,使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的!
1 /** 2 * StampedLock的使用 3 */ 4 package thread02; 5 6 import java.util.concurrent.locks.StampedLock; 7 8 public class StampedLockTest01 9 { 10 public static void main(String[] args) 11 { 12 Point point = new Point(); 13 point.move(1, 2); 14 System.out.println(point.distanceFromOrigin()); 15 } 16 } 17 18 class Point 19 { 20 // 一个点的x,y坐标 21 private double x, y; 22 23 /** 24 * Stamped类似一个时间戳的作用,每次写的时候对其+1来改变被操作对象的Stamped值 25 * 这样其它线程读的时候发现目标对象的Stamped改变,则执行重读 26 */ 27 private final StampedLock stampedLock = new StampedLock(); 28 29 void move(double deltaX, double deltaY) 30 { 31 /** 32 * stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化 33 * 即每次+1,直到加到最大值,然后从0重新开始; 34 */ 35 long stamp = stampedLock.writeLock(); // 加写锁 36 // System.out.println(stamp); 37 try 38 { 39 x += deltaX; 40 y += deltaY; 41 } 42 finally 43 { 44 stampedLock.unlockWrite(stamp); // 释放写锁 45 } 46 47 // System.out.println("x:" + x + " y:" + y); 48 } 49 50 // 乐观读锁 51 double distanceFromOrigin() 52 { 53 /** 54 * tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写 55 * 每次读的时候得到一个当前的stamp值(类似时间戳的作用) 56 */ 57 long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观锁 58 59 //这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断 60 double currentX = x, currentY = y; // 将两个字段读入本地局部变量 61 62 /** 63 * 如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读, 64 * 在重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁) 65 * 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式 66 * 读锁又重新返回一个stampe值 67 */ 68 if (!stampedLock.validate(stamp)) // 检查发出乐观读锁后同时是否有其他写锁发生 69 { 70 stamp = stampedLock.readLock(); // 如果没有,我们再次获得一个读悲观锁 71 try 72 { 73 currentX = x; // 将两个字段读入本地局部变量 74 currentY = y; // 将两个字段读入本地局部变量 75 } 76 finally 77 { 78 stampedLock.unlockRead(stamp); // 释放读锁 79 } 80 } 81 82 // System.out.println("x:" + x + " y:" + y); 83 //读锁验证成功后才执行计算,即读的时候没有发生写 84 return Math.sqrt(currentX * currentX + currentY * currentY); 85 } 86 87 // 悲观读锁 88 void moveIfAtOrigin(double newX, double newY) 89 { 90 long stamp = stampedLock.readLock(); 91 try 92 { 93 while (x == 0.0 && y == 0.0) // 循环,检查当前状态是否符合 94 { 95 long ws = stampedLock.tryConvertToWriteLock(stamp); // 将读锁转为写锁 96 if (ws != 0L) // 这是确认转为写锁是否成功 97 { 98 stamp = ws; // 如果成功,替换票据 99 x = newX; // 进行状态改变 100 y = newY; // 进行状态改变 101 break; 102 } 103 else // 如果不能成功转换为写锁 104 { 105 stampedLock.unlockRead(stamp); // 我们显式释放读锁 106 stamp = stampedLock.writeLock(); // 显式直接进行写锁,然后再通过循环再试 107 } 108 } 109 } 110 finally 111 { 112 stampedLock.unlock(stamp); // 释放读锁或写锁 113 } 114 } 115 }
死锁:
在两段不同的逻辑都在等待对方的锁释放才能继续往下工作时,这个时候就会产生死锁;
1 /** 2 * 死锁 3 */ 4 package thread02; 5 6 class Count11 7 { 8 private byte[] lock1 = new byte[1]; 9 private byte[] lock2 = new byte[2]; 10 11 private int num = 0; 12 13 public void add1() 14 { 15 synchronized (lock1) 16 { 17 try 18 { 19 Thread.sleep(1001); // 模拟干活 20 } 21 catch (InterruptedException e) 22 { 23 e.printStackTrace(); 24 } 25 26 synchronized (lock2) // 产生死锁,一直在等待lock2对象释放锁 27 { 28 num += 1; 29 } 30 31 System.out.println(Thread.currentThread().getName() + " - " + num); 32 } 33 } 34 35 public void add2() 36 { 37 synchronized (lock2) 38 { 39 try 40 { 41 Thread.sleep(1001); 42 } 43 catch (InterruptedException e) 44 { 45 e.printStackTrace(); 46 } 47 48 synchronized (lock1) // 产生死锁,一直等待lock1对象释放锁 49 { 50 num += 1; 51 } 52 53 System.out.println(Thread.currentThread().getName() + " - " + num); 54 } 55 } 56 } 57 58 public class DeadLockTest01 59 { 60 public static void main(String[] args) 61 { 62 Count11 count = new Count11(); 63 64 ThreadA threadA = new ThreadA(count); 65 Thread t1 = new Thread(threadA); 66 t1.setName("线程A"); 67 68 ThreadB threadB = new ThreadB(count); 69 Thread t2 = new Thread(threadB); 70 t2.setName("线程B"); 71 } 72 73 } 74 75 class ThreadA implements Runnable 76 { 77 private Count11 count; 78 79 public ThreadA(Count11 count) 80 { 81 this.count = count; 82 } 83 84 @Override 85 public void run() 86 { 87 count.add1(); 88 } 89 } 90 91 class ThreadB implements Runnable 92 { 93 private Count11 count; 94 95 public ThreadB(Count11 count) 96 { 97 this.count = count; 98 } 99 100 @Override 101 public void run() 102 { 103 count.add2(); 104 } 105 }
volatile:
表面意思:易变的,不稳定的;
作用:修饰变量;告诉编译器,凡是被该关键字声明的变量都是易变的,不稳定的;所以不要试图对该变量使用缓存等优化机制,而应当每次都从它的内存地址中去读取值;
特性:
使用volatile标记的变量在读取或写入时不需要使用锁,这将减少产生死锁的概率,使代码保持整洁;
每次读取volatile的变量都要从它的内存地址中读取,但并不是每次修改完volatile的变量后都要立刻将它的值写回内存;也就是说volatile只提供内存可见性,而没有提供原子性;所以使用这个关键字做高并发的安全机制是不可靠的;
适用场景:
最好是那种只有一个线程修改变量,多个线程读取该变量的地方;也就是对内存可见性要求高,而对原子性要求低的地方;
volatile与加锁机制的主要区别:
加锁机制既可以保证可见性又可以确保原子性;而volatile变量只能保证可见性;
atomic:
详述:
atomic是不会阻塞线程(或者说只是在硬件级别上阻塞了),线程安全的加强版的volatile原子操作;
java.util.concurrent.atomic包里,多了一批原子操作,主要用于高并发环境下的高效程序处理;
1 /** 2 * 原子操作 3 */ 4 package thread02; 5 6 import java.util.concurrent.atomic.AtomicInteger; 7 8 public class AtomicIntegerTest01 9 { 10 public static void main(String[] args) 11 { 12 AtomicInteger ai = new AtomicInteger(0); 13 14 // 获取当前的值 15 System.out.println(ai.get()); 16 System.out.println("--------------"); 17 18 // 取当前的值,并设置新的值 19 System.out.println(ai.getAndSet(5)); 20 System.out.println(ai.get()); // 设置新值之后的当前值 21 System.out.println("--------------"); 22 23 // 获取当前的值,并自增 24 System.out.println(ai.getAndIncrement()); 25 System.out.println(ai.get()); // 自增之后的当前值 26 System.out.println("--------------"); 27 28 // 获取当前的值,并自减 29 System.out.println(ai.getAndDecrement()); 30 System.out.println(ai.get()); // 自减之后的当前值 31 System.out.println("--------------"); 32 33 // 获取当前的值,并加上预期的值 34 System.out.println(ai.getAndAdd(3)); 35 System.out.println(ai.get()); // 加上预期值之后的当前值 36 System.out.println("--------------"); 37 38 } 39 }
单例:
1 /** 2 * 单例模式第一种写法:线程不安全的,不正确的写法 3 */ 4 package thread02.singleton; 5 6 public class Singleton01 7 { 8 private static Singleton01 instance; 9 10 private Singleton01() 11 { 12 13 } 14 15 public static Singleton01 getSingleton() 16 { 17 if(instance == null) 18 { 19 instance = new Singleton01(); 20 } 21 22 return instance; 23 } 24 }
1 /** 2 * 单例模式第二种写法:线程安全,但是高并发性能不是很高 3 */ 4 package thread02.singleton; 5 6 public class Singleton02 7 { 8 private static Singleton02 instance; 9 10 private Singleton02() 11 { 12 13 } 14 15 public static synchronized Singleton02 getSingleton() 16 { 17 if(instance == null) 18 { 19 instance = new Singleton02(); 20 } 21 22 return instance; 23 } 24 }
1 /** 2 * 单例模式第三种写法:线程安全,性能又高,这种写法最为常用 3 */ 4 package thread02.singleton; 5 6 public class Singleton03 7 { 8 private static Singleton03 instance; 9 private static byte[] lock = new byte[0]; 10 11 private Singleton03() 12 { 13 14 } 15 16 public static Singleton03 getSingleton() 17 { 18 if(instance == null) 19 { 20 synchronized (lock) 21 { 22 if(instance == null) 23 { 24 instance = new Singleton03(); 25 } 26 } 27 } 28 29 return instance; 30 } 31 }
1 /** 2 * 单例模式第四种写法:线程安全,性能又高,也是最为常用的 3 */ 4 package thread02.singleton; 5 6 import java.util.concurrent.locks.ReentrantLock; 7 8 public class Singleton04 9 { 10 private static Singleton04 instance; 11 private static ReentrantLock lock = new ReentrantLock(); 12 13 private Singleton04() 14 { 15 16 } 17 18 public static Singleton04 getSingleton() 19 { 20 if(instance == null) 21 { 22 lock.lock(); 23 24 if(instance == null) 25 { 26 instance = new Singleton04(); 27 } 28 29 lock.unlock(); 30 } 31 32 return instance; 33 } 34 }