同步与锁
上一篇中,笔者介绍了Java多线程的基础知识,主要讲解了进程/线程的区别、Java多线程的创建、Java多线程的使用,以及Java多线程的生命周期。
对于多线程,笔者想强调一点的是,多线程的使用并不是为了提高程序的运行速度,而是为了提高程序的运行效率,让CPU的使用率更高,让资源得到更合理的安排。
如果你对上述的知识点,还不了解,那笔者建议还是先从多线程--基础入手,再来学习本篇文章。
今天,我们来继续学习Java多线程的内容---同步与锁。
线程安全
多线程的使用,让CPU得到了更充分的释放,让程序有了更高的执行效率,说直白点这叫释放天性。但是,多线程在使用的同时,也带来了一些烦恼和麻烦。俗话说的好,世界上任何事物都包含有利的一面和不利的一面,事物的两面性。
对于多线程来说,也遵循同样的道理,而不好的一面就是安全问题。
(1)什么是线程的安全问题?
(2)什么情况下会导致线程的安全问题?
解决问题之前,我们需要知道上面两个问题的答案。
问题一:
一个线程从开始执行到执行结束的过程,如果在执行过程中有一个对象的变量被其他线程锁修改,那么对于当前线程来说,就发生了线程安全问题。如果每次运行结果和单线程运行的结果不一致,那么就会产生线程安全问题。
问题二:
运行多线程环境;
多线程中存在共享变量;
多线程中操作共享变量,主要操作行为--写操作;
我们都知道,CPU在任何一个时间点上都只会操作一个线程,我们感受到多个程序同时执行的情况,只不过是CPU模拟出来的结果,由于CPU处理的速度很快,在一秒中内CPU可以执行很多指令,处理很多线程,所以我们误认为是CPU在同时执行。
当一个线程在被执行时,不巧的是CPU在切换时,把执行权切换到了另一条线程上,处理了另外一个线程的请求,而另外的线程对共享数据做了改变。当CPU再次切换回原有的线程上时,共享的数据已被改变,数据安全问题由此诞生。
我们举个简单例子,来验证:
//卖票的例子:
public class ThreadTest9 implements Runnable {
//可售总数量 100张:
private Integer count = 100;
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(true) {
//如果大于0,就进行卖票
if(count>0) {
System.out.println(Thread.currentThread().getName()+"正在售票,剩余票数:"+ count--);
continue;
}
System.out.println(Thread.currentThread().getName()+"票源不足,此时可售票数为:"+ count);
break;
}
}
public static void main(String[] agrs){
ThreadTest9 ticket = new ThreadTest9();
//开启4个窗口来卖票
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
new Thread(ticket,"窗口4").start();
}
}
测试结果:
窗口3正在售票,剩余票数:100
窗口4正在售票,剩余票数:100
窗口2正在售票,剩余票数:100
窗口1正在售票,剩余票数:99
窗口2正在售票,剩余票数:98
。。。。。。。。。。。。。。。省略
窗口2正在售票,剩余票数:8
窗口3正在售票,剩余票数:7
窗口1正在售票,剩余票数:6
窗口4正在售票,剩余票数:7
窗口2正在售票,剩余票数:5
窗口4正在售票,剩余票数:4
窗口4正在售票,剩余票数:1
窗口4票源不足,此时可售票数为:0
窗口2正在售票,剩余票数:44
窗口3正在售票,剩余票数:98
窗口2票源不足,此时可售票数为:-1
窗口1正在售票,剩余票数:0
窗口1票源不足,此时可售票数为:-1
窗口3票源不足,此时可售票数为:-1
在run()方法中,加入Thread.sleep(10)的主要目的是为了让四个线程都能得到执行。
通过测试结果,可以看到不同的窗口卖了相同的票,而在可售票源为0时,依旧有线程在做--操作。
如何解决线程的安全问题?
消除共享数据:在多线程中,不同线程间可共享的数据包括“成员变量、静态变量”,将这两种类型的变量转换为局部变量,那么线程安全问题自然就解决了。
建立副本:使用ThreadLocal为每一个线程建立一个变量的副本,将共享变量存入到ThreadLocal当中,这样以来实现每个线程间独立操作,算是上面解决方案的一种升华。
使用同步:在线程执行逻辑中,给读写共享变量的操作同时加锁处理,使得同一时刻只有一个线程可以操作访问共享数据。
下面,就主要来讲解下加锁处理线程安全!!!
解决线程的安全问题,主要是通过加锁来处理,加锁的目的就是为了保证一个线程在操作共享变量时,如果没执行完,那其他线程不能执行。简单点说就是一个线程在执行,其他线程给我等着,我不执行完,你们靠边呆着去。
在Java中,主要有三种方式来进行加锁,以来保证线程的安全:
同步方法:在类中的方法上加synchronized修饰符;
public synchronized void test(){
//代码逻辑
}
同步方法中的锁是:this,即调用者本身;
静态同步方法: 在类中的静态方法上加synchronized
public static synchronized void test(){
//代码逻辑
}
静态同步方法中的锁是:类名.class,即Class对象;
同步代码块:在需要同步的代码上写一个synchronized(Object obj){}代码块;
synchronized(Object o){
//代码逻辑
}
同步代码块中的锁是:任意对象;
接下来,我们通过加锁的方式,再进行卖票:
//卖票的例子:
public class ThreadTest9 implements Runnable {
//可售总数量 100张:
private Integer count = 100;
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(true) {
//如果大于0,就进行卖票
if(count>0) {
synchronized (this) {
if(count>0){
System.out.println(Thread.currentThread().getName() + "正在售票,剩余票数:" + count--);
continue;
}
}
}
System.out.println(Thread.currentThread().getName()+"票源不足,此时可售票数为:"+ count);
break;
}
}
public static void main(String[] agrs){
ThreadTest9 ticket = new ThreadTest9();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
new Thread(ticket,"窗口4").start();
}
}
测试结果:
窗口2正在售票,剩余票数:100
窗口2正在售票,剩余票数:99
窗口1正在售票,剩余票数:98
窗口1正在售票,剩余票数:97
窗口3正在售票,剩余票数:96
窗口4正在售票,剩余票数:95
。。。。。。。。。。。。。。省略
窗口2正在售票,剩余票数:4
窗口2正在售票,剩余票数:3
窗口1正在售票,剩余票数:2
窗口4正在售票,剩余票数:1
窗口4票源不足,此时可售票数为:0
窗口1票源不足,此时可售票数为:0
窗口3票源不足,此时可售票数为:0
窗口2票源不足,此时可售票数为:0
synchronized的使用,解决了线程的安全问题,但是随着而来的是程序性能的下降。正所谓,事物的两面性。
单例设计模式
本文的最高,我们来说下面试中常见的一个问题:写一个性能安全的单例。
不安全的单例:
public class ThreadTest10 {
private static ThreadTest10 single;
private ThreadTest10(){}
//懒汉:
public static ThreadTest10 getInstance(){
if(single==null){
single = new ThreadTest10();
}
return single;
}
}
安全的单例:
//无锁版:
public class ThreadTest11 {
private static ThreadTest11 single = new ThreadTest11();
private ThreadTest11(){}
public static ThreadTest11 getInstance(){
return single;
}
}
//加锁版:
public class ThreadTest13 {
//创建一把锁
private static Object obj = new Object();
private static ThreadTest13 threadTest13;
private ThreadTest13(){}
public static ThreadTest13 getInstance(){
if(threadTest13==null){
synchronized (obj){
//双判断:
if(threadTest13==null){
threadTest13 = new ThreadTest13();
}
}
}
return threadTest13;
}
}
升级版:
后面说。。。。。