一、线程安全
1.线程安全的概念
线程安全:某个类被单个线程,或者多个线程同时访问,所表现出来的行为是一致,则可以说这个类是线程安全的。
2.什么情况下会出现线程安全问题
在单线程中不会出现线程安全问题,在多线程编程的情况下,并且多个线程访问同一资源的情况下可能出现线程安全问题。如下面的例子,出现典型的线程安全问题:
1 public class BookSaleRunable implements Runnable{ 2 private int bookNum=10;//书的总数为10 3 @Override 4 public void run() { 5 for(int i=0;i<5;i++){ 6 if(bookNum>0){ 7 try { 8 Thread.sleep(3000); 9 } catch (InterruptedException e) { 10 e.printStackTrace(); 11 } 12 System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum)); 13 } 14 } 15 16 } 17 18 public static void main(String[] args) { 19 BookSaleRunable booksr = new BookSaleRunable(); 20 //开启3个线程 21 Thread t1 = new Thread(booksr); 22 Thread t2 = new Thread(booksr); 23 Thread t3 = new Thread(booksr); 24 t1.start(); 25 t2.start(); 26 t3.start(); 27 } 28 29 }
结果输出:
Thread-1 卖出一本书,剩余书籍:9 Thread-2 卖出一本书,剩余书籍:7 Thread-0 卖出一本书,剩余书籍:8 Thread-1 卖出一本书,剩余书籍:6 Thread-0 卖出一本书,剩余书籍:4 Thread-2 卖出一本书,剩余书籍:5 Thread-1 卖出一本书,剩余书籍:3 Thread-2 卖出一本书,剩余书籍:1 Thread-0 卖出一本书,剩余书籍:2 Thread-2 卖出一本书,剩余书籍:0 Thread-1 卖出一本书,剩余书籍:-1 Thread-0 卖出一本书,剩余书籍:-2
上例出现超卖现象,在生活中不允许,同时如果是一个线程肯定不会出现。那么如何解决这个问题呢?基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案,在给定时刻只允许一个任务访问共享资源。
二、解决线程安全问题的方法
1.synchronized关键字
synchronized,顾名思义就是同步的意思,主要用来给方法、代码块加锁。当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。synchronized可以用来修饰方法和代码块。
synchronized修饰方法
当synchronized关键字修饰方法的时候,这个方法我们称为同步方法,同步方法可以控制对类成员变量的方法,synchronized关键字表明方法已经加锁,当任意一个线程访问同步方法的时候都必须 判断这个方法是否被其他线程独占。所有对象自动含有单一的锁,当在对象上调用其任意的synchronized方法的时候,次对象被加锁,这个时候这个对象的其他synchronized方法必须等到前一个同步方法调用完毕并释放锁之后才能被调用。如下:
1 public class BookSaleRunable2 implements Runnable{ 2 private int bookNum=20;//书的总数为10 3 @Override 4 public void run() { 5 for(int i=0;i<7;i++){ 6 saleBook(); 7 } 8 9 } 10 11 public static void main(String[] args) { 12 BookSaleRunable2 booksr = new BookSaleRunable2(); 13 //开启3个线程 14 Thread t1 = new Thread(booksr); 15 Thread t2 = new Thread(booksr); 16 Thread t3 = new Thread(booksr); 17 t1.start(); 18 t2.start(); 19 t3.start(); 20 } 21 22 private synchronized void saleBook(){ 23 if(bookNum>0){ 24 try { 25 Thread.sleep(300); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum)); 30 } 31 } 32 33 }
结果输出:
Thread-0 卖出一本书,剩余书籍:19 Thread-0 卖出一本书,剩余书籍:18 Thread-0 卖出一本书,剩余书籍:17 Thread-0 卖出一本书,剩余书籍:16 Thread-0 卖出一本书,剩余书籍:15 Thread-0 卖出一本书,剩余书籍:14 Thread-0 卖出一本书,剩余书籍:13 Thread-2 卖出一本书,剩余书籍:12 Thread-2 卖出一本书,剩余书籍:11 Thread-2 卖出一本书,剩余书籍:10 Thread-2 卖出一本书,剩余书籍:9 Thread-2 卖出一本书,剩余书籍:8 Thread-2 卖出一本书,剩余书籍:7 Thread-2 卖出一本书,剩余书籍:6 Thread-1 卖出一本书,剩余书籍:5 Thread-1 卖出一本书,剩余书籍:4 Thread-1 卖出一本书,剩余书籍:3 Thread-1 卖出一本书,剩余书籍:2 Thread-1 卖出一本书,剩余书籍:1 Thread-1 卖出一本书,剩余书籍:0
注意:在使用并发的时候,将域设置为private是非常重要的,否则,synchronized关键字不能防止其他任务直接访问域,这样讲产生冲突问题。
synchronized与static
关键字synchronized可以修饰static静态方法,这个时候是对当前静态方法所属的Class类进行加锁。与修饰普通方法是有差别的。如下:
1 public class ServiceDemo { 2 3 public static void print1(){ 4 System.out.println(Thread.currentThread().getName()+" print1 start "); 5 System.out.println(Thread.currentThread().getName()+" print1 end "); 6 } 7 public static void print2(){ 8 System.out.println(Thread.currentThread().getName()+" print2 start "); 9 System.out.println(Thread.currentThread().getName()+" print2 end "); 10 } 11 12 public static void main(String[] args) { 13 ServiceDemo serviceDemo1 = new ServiceDemo(); 14 ServiceDemo serviceDemo2 = new ServiceDemo(); 15 Thread t1 =new Thread(new Thread1(serviceDemo1)); 16 Thread t2 =new Thread(new Thread2(serviceDemo2)); 17 t1.start(); 18 t2.start(); 19 } 20 } 21 22 class Thread1 implements Runnable{ 23 private ServiceDemo serviceDemo; 24 25 Thread1(ServiceDemo serviceDemo){ 26 this.serviceDemo = serviceDemo; 27 } 28 29 @Override 30 public void run() { 31 serviceDemo.print1(); 32 } 33 34 } 35 class Thread2 implements Runnable{ 36 private ServiceDemo serviceDemo; 37 38 Thread2(ServiceDemo serviceDemo){ 39 this.serviceDemo = serviceDemo; 40 } 41 42 @Override 43 public void run() { 44 serviceDemo.print2(); 45 } 46 47 }
输出结果:
Thread-0 print1 start Thread-1 print2 start Thread-0 print1 end Thread-1 print2 end
使用static和synchronized结合的时候,效果完全不同了,如下:
1 public class ServiceDemo { 2 3 public synchronized static void print1(){ 4 System.out.println(Thread.currentThread().getName()+" print1 start "); 5 System.out.println(Thread.currentThread().getName()+" print1 end "); 6 } 7 public synchronized static void print2(){ 8 System.out.println(Thread.currentThread().getName()+" print2 start "); 9 System.out.println(Thread.currentThread().getName()+" print2 end "); 10 } 11 12 public static void main(String[] args) { 13 ServiceDemo serviceDemo1 = new ServiceDemo(); 14 ServiceDemo serviceDemo2 = new ServiceDemo(); 15 Thread t1 =new Thread(new Thread1(serviceDemo1)); 16 Thread t2 =new Thread(new Thread2(serviceDemo2)); 17 t1.start(); 18 t2.start(); 19 } 20 } 21 22 class Thread1 implements Runnable{ 23 private ServiceDemo serviceDemo; 24 25 Thread1(ServiceDemo serviceDemo){ 26 this.serviceDemo = serviceDemo; 27 } 28 29 @Override 30 public void run() { 31 serviceDemo.print1(); 32 } 33 34 } 35 class Thread2 implements Runnable{ 36 private ServiceDemo serviceDemo; 37 38 Thread2(ServiceDemo serviceDemo){ 39 this.serviceDemo = serviceDemo; 40 } 41 42 @Override 43 public void run() { 44 serviceDemo.print2(); 45 } 46 47 }
输出结果:
Thread-0 print1 start Thread-0 print1 end Thread-1 print2 start Thread-1 print2 end
从结果看出,两个线程并未共享同一个对象,但是两个不同对象的静态同步方法发生了同步互斥。如果将ServiceDemo类中的一个方法修改为非静态同步方法,上述代码的执行结果则可能不是上面显示的那样了,那是因为如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。
synchronized修饰代码块
使用关键字synchronized声明方法在某些情况下是有弊端的,非常影响效率。如果多个线程在访问一个synchronized方法,那么同一时刻只有一个线程在执行该方法,而其他线程都必须等待,但是如果该方法没有使用synchronized,则所有线程可以在同一时刻执行它,减少了执行的总时间。在这样的情况下,推荐使用同步语句块来解决,同步方法是对当前对象进行加锁,而synchronized代码块是对某一个对象加锁。synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。
1 public class BookSaleRunable3 implements Runnable{ 2 private int bookNum=20;//书的总数为10 3 @Override 4 public void run() { 5 for(int i=0;i<7;i++){ 6 synchronized (this) { 7 if(bookNum>0){ 8 try { 9 Thread.sleep(300); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:"+(--bookNum)); 14 } 15 } 16 } 17 18 } 19 20 public static void main(String[] args) { 21 BookSaleRunable3 booksr = new BookSaleRunable3(); 22 //开启3个线程 23 Thread t1 = new Thread(booksr); 24 Thread t2 = new Thread(booksr); 25 Thread t3 = new Thread(booksr); 26 t1.start(); 27 t2.start(); 28 t3.start(); 29 } 30 31 }
结果输出:
Thread-0 卖出一本书,剩余书籍:19 Thread-0 卖出一本书,剩余书籍:18 Thread-0 卖出一本书,剩余书籍:17 Thread-0 卖出一本书,剩余书籍:16 Thread-0 卖出一本书,剩余书籍:15 Thread-0 卖出一本书,剩余书籍:14 Thread-0 卖出一本书,剩余书籍:13 Thread-2 卖出一本书,剩余书籍:12 Thread-2 卖出一本书,剩余书籍:11 Thread-2 卖出一本书,剩余书籍:10 Thread-2 卖出一本书,剩余书籍:9 Thread-1 卖出一本书,剩余书籍:8 Thread-1 卖出一本书,剩余书籍:7 Thread-1 卖出一本书,剩余书籍:6 Thread-1 卖出一本书,剩余书籍:5 Thread-1 卖出一本书,剩余书籍:4 Thread-1 卖出一本书,剩余书籍:3 Thread-1 卖出一本书,剩余书籍:2 Thread-2 卖出一本书,剩余书籍:1 Thread-2 卖出一本书,剩余书籍:0
总结:
- 当一个线程访问对象的一个synchronized方法的时候,另外的线程仍然可以访问对象的非synchronized方法。
- 当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
- 同一时间只有一个线程可以执行synchronized同步代码块中的代码。
- 如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。
- synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象
2.Lock锁机制
Lock对象必须被显式的创建、锁定和释放,与jvm内建的锁相比,代码缺乏优雅性,但是对于解决某些特殊类型的问题来说,它更加灵活。使用synchronized关键字,需要些的代码量更少。具体的例子如下:
1 public class BookSaleRunable4 implements Runnable { 2 private int bookNum = 10;// 书的总数为10 3 private Lock lock = new ReentrantLock();// 重入锁 4 5 @Override 6 public void run() { 7 for (int i = 0; i < 3; i++) { 8 lock.lock();// 显示加锁 9 try { 10 if (bookNum > 0) { 11 Thread.sleep(300); 12 System.out.println(Thread.currentThread().getName()+" 卖出一本书,剩余书籍:" + (--bookNum)); 13 } 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 }finally{ 17 lock.unlock();//主动释放锁,很容易忘记释放锁 18 } 19 } 20 21 } 22 public static void main(String[] args) { 23 BookSaleRunable4 booksr = new BookSaleRunable4(); 24 // 开启3个线程 25 Thread t1 = new Thread(booksr); 26 Thread t2 = new Thread(booksr); 27 Thread t3 = new Thread(booksr); 28 t1.start(); 29 t2.start(); 30 t3.start(); 31 } 32 33 }
结果输出:
Thread-0 卖出一本书,剩余书籍:9 Thread-0 卖出一本书,剩余书籍:8 Thread-0 卖出一本书,剩余书籍:7 Thread-2 卖出一本书,剩余书籍:6 Thread-2 卖出一本书,剩余书籍:5 Thread-2 卖出一本书,剩余书籍:4 Thread-1 卖出一本书,剩余书籍:3 Thread-1 卖出一本书,剩余书籍:2 Thread-1 卖出一本书,剩余书籍:1