前言:
在我们学习多线程的过程中,总会遇到了一问题那就是线程安全问题,那么,什么是线程安全问题,怎样去判断是否存在线程安全问题,在知道了线程安全问题,开发中我们有如何去避免出现线程安全等问题,接下来我们具体了解一下关于线程安全这方面的一些知识。
一、什么是线程安全
通常理解就是多个线程同时操作一个实现了Runnable接口的类,程序每一次调用的结果和单线程调用的结果都是一样的,则线程是安全的的,否则线程不安全:来看一下下面的例子:
public class ThreadDemo { public static int number = 0; public static void main(String[] args) { Runnable task1 = () -> { for(int i = number; i< 5; i++){ number++; System.out.println(number); } }; Thread thread1 = new Thread(task1); Thread thread2 = new Thread(task1); thread1.start(); thread2.start(); } }
前面一段代码我们开启了两个线程来运行task任务,在我们运行多次后就会出下如下截图中某个数字输出了多次,而且还出现了大于判定条件的数字,这显然是不正常的。
1
3
4
5
6
2
7
8
9
Process finished with exit code 0
上面的例子只是演示了一种造成线程不安全的因素,就是多操作共享数据的线程有多个,我们都清楚CPU在执行任务时时并发执行,也就是抢占式,一个线程获取到了CPU资源,运行到了一半,其他线程抢到了CPU执行权,而两者正好操作的都是同一部分内容,就会出现线程安全问题,以下几种情况都会造成线程安全问题:
1、多个线程在操作共享数据:
2、操作共享数据的线程代码有多条:
3、多线程对共享数据有写操作:
二、问题的解决:
前面我们分析了一下产生线程不安全的原因,就是多个线程操作共享资源时,线程之间频繁的抢占导致数据的不唯一性,要解决线程安全问题,那么我们只要保证在其中一个线程操作共享资源时,其他线程不能对此共享资源进行访问,必须等候其操作完后其他线程才能进行访问,这样就可以保证数据的同步性,从而解决问题,知道了问题的原因和解决的方式,那么我们来看一下java为我们保证线程的安全,提供了那些方式。
1、同步代码快(synchronize):2、同步代方法(synchronize):3、同步锁(ReentrantLock):4、特殊局部变量(volatile):5、局部变量(ThreadLocal):6、阻塞式队列(LinkedBlockingQueue):7原子变量(Atomic)。
三、同步代码快(synchronize)
在java开发中我们解决线程安全问题,最常见的关键字就是synchronize,他的作用就是为一段代码加一个锁,当一个前线方访问这段代码时会获取到锁,只如果这时候有其他线程访问这块代码,由于获取不到锁进入阻塞状态,只有前一个线程执行完成释放锁后才会获取锁,稍微修改一下上面的代码。
public class ThreadDemo { public static int number = 0; public static void main(String[] args) { Runnable task1 = () -> { synchronized (ThreadDemo.class){ for(int i = number; i< 5; i++){ number++; System.out.println(number); } } }; Thread thread1 = new Thread(task1); Thread thread2 = new Thread(task1); thread1.start(); thread2.start(); } }
打印结果:从结果来看我们加上synchronize关键字后实现了我们正常化的输出操作
1
2
3
4
5
Process finished with exit code 0
四、同步方法(synchronize)
在这里同步方法和同步代码快是一样的,只不过一个是在方法体上加synchronize关键字一个是在需要一段代码上枷锁,改正一下上面的例子来看一下:
public class SynchronizeDemo implements Runnable { public static int number = 0; @Override public void run() { test(); } public synchronized void test(){ for(int i = number; i< 5; i++){ number++; System.out.println(number); } } }
public class ThreadDemo { public static void main(String[] args) { SynchronizeDemo demo = new SynchronizeDemo(); Thread thread1 = new Thread(demo); Thread thread2 = new Thread(demo); thread1.start(); thread2.start(); } }
打印结果:
1
2
3
4
5
Process finished with exit code 0
五、同步锁(ReentrantLock)
ReentrantLock是在java.util.concurrent.Lock这个包下,相比较synchronize代码块和方法提供了更加广泛的锁定操作,同步代码块/同步方法具有的功能,Lock锁都已拥有,除此之外更强大,更加体现了java面向对象的思想。
Lock接口中有两个常用的方法lock(枷锁),unlock(释放锁),ReentrantLock类的构造参数传入的是一个Boolean类型的值,false:表示非公平锁,指一个线程拿到了锁如果他不主动释放或者执行完毕其他线程永远拿不到锁,true:表示公平锁,指所有线程公平的拥有执行权,来看一段代码的执行。
public class ReenreantLookDemo { private static int number = 10; private static Lock lock = new ReentrantLock(true); public static void main(String[] args) { Runnable task = () -> { while (true){ lock.lock(); if(number> 0){ try{ Thread.sleep(100); }catch(Exception e){ e.printStackTrace(); }finally { lock.unlock(); } System.out.println("当前执行线程" + Thread.currentThread().getName() + "-" + number-- ); } } }; Thread thread1= new Thread(task,"逝清雪"); Thread thread2= new Thread(task,"莫问"); thread1.start(); thread2.start(); } }
打印结果:
当前执行线程逝清雪-10 当前执行线程莫问-9 当前执行线程逝清雪-8 当前执行线程莫问-7 当前执行线程逝清雪-6 当前执行线程莫问-5 当前执行线程逝清雪-4 当前执行线程莫问-3 当前执行线程逝清雪-2 当前执行线程莫问-1 当前执行线程逝清雪-0
六、总结:
上面我们学了synchronize和Lock两种保证线程安全的方式,来看一下他们来看一下他们之间具体有哪些相同和不同:
1、synchronize是一个java内置的关键字,在jvm层面,而Lock是一个接口
2、synchronize无法判断是否获取到锁的状态,而Lock可以
3、synchronize在执行完任务后是有jvm控制锁的释放,而Lock则是需要我们手动释放锁
4、synchronize如果存在两个线程,线程1获取锁,线程2等待,若线程1阻塞,则线程2会一直等下去,Lock锁就不一定会等下去,如果线程2尝试获取不到锁,线程可以不用等待,直 接结束
5、synchronize锁,可重如入、不可中断、非公平,而Lock锁,可重入、可中断、非公平和公平锁都支持
6、synchronizes适合有少量同步代码的问题,Lock适合有大量同步代码的问题