一.并发编程的三大问题
一.可见性
1.概念:
对于共享数据,一个线程对该数据的值进行修改后另一个线程无法得到修改后的值
2.代码演示:
public class Test1 {
private static boolean flag=true;
public static void main(String[] args) throws InterruptedException {
new Thread(){
@Override
public void run() {
while (flag){
;
}
System.out.println("flag已修改");
}
}.start();
Thread.sleep(2000);
new Thread(){
@Override
public void run() {
flag=false;
System.out.println("已经被我修改");
}
}.start();
}
}
运行结果:
不难发现,当线程二将flag改为true的时候线程一并不能提前得到最新数据,导致程序依旧停留在while循环中
3.原因分析
线程工作原理:在线程操作变量时,会先将主内存中的变量拷贝到工作内存中,然后对工作内存中的拷贝变量进行赋值计算等操作,操作完之后再赋值给主内存
分析:比如示例代码的上下两个线程分别表示线程1和线程2,线程一的工作内存复制的值是true,当线程2将flag改为false并且赋给主内存后,线程一并不会去读取主内存中的值,这也就导致了线程一卡在while循环无法出来
4.解决方法
方式一:用volatile修饰共享变量
原理:被volatile修饰的变量,当主内存中该变量被别的线程二改变时,会将工作内存中的变量全部作废,这样线程一要重新向主内存中去读取数据的值
给flag添加了volatile之后代码的运行结果:
方式二:用synchronized来保证可见性
仅就红色区域为新添的代码(synchronized的参数要放一个对象),也能解决这个问题
原理分析:
sychronized的时候会有lock和unlock的操作,在lock的时候会刷新当前线程工作内存中的值,实现工作内存及时读取最新的数据值
补充:正因为如此,所以如果仅仅只在while循环中加一句
System.out.println("true");
最后的也能退出循环,因为println的源码中有用到synchronized
二.原子性
1.概念:
在一次或多次操作中,要么所有操作都执行,而且不会因为互相干扰出现异常结果,要么都不执行 (经典抢车票中的线程安全问题)
2.代码演示(代码选自 创建多线程的四种方式中的方式二):
//1.创建一个实现Runnable接口的类
class Mythread implements Runnable{
private int ticket=100;
//2.重写run方法
@Override
public void run() {
while (true){
if(ticket>=1){
System.out.println(Thread.currentThread().getName()+"抢到票"+ticket);
ticket--;
}
else {
System.out.println(Thread.currentThread().getName()+"票已售罄");
break;
}
}
}
}
public class Test1 {
public static void main(String[] args) {
//3.创建一个实现类的对象
Mythread t= new Mythread();
/*4.每个线程各自创建一个以实现类对象为参数的Thread
类的对象*/
Thread t1=new Thread(t);
Thread t2=new Thread(t);
t1.setName("线程一");
t2.setName("线程二");
//5.start开启进程
t1.start();
t2.start();
}
}
运行结果:
不难发现,产生了重票
3.原因分析:
由于可能在同一时间多个线程同时对共享变量进行操作,正是由于同时操作,所以导致得到的票重复之类的问题
4.解决方法
方式一(syncronized):用synchronized进行同步的代码块,哪个进程拿到锁就哪个进程执行,如果没拿到的只能在外面等着,这保证了代码块最多只能有一个进程在执行
例如:对于上面的代码示例中while内部用synchronized包起来,这样当线程一拿到锁开始抢票的时候其他线程都在while那里等着不往下执行,等线程一执行完代码块后锁释放出来再和其他进程一起去抢
注意: synchronized如果是把while也包进来了结果会是只有一个进程直接把所有票抢完,原因可以结合上面的原理自行分析
所以使用synchronized有一个原则:不能包的太多也不能太少
三.有序性
1.概念:
代码执行顺序不是严格按照顺序从上到下执行的
2.代码示例():
ready=true;
num=2;
经过重排序,执行顺序变成
num=2;
ready=true
此时,如果线程二先执行并且重排序的话可能导致r.r1=0,我们要做得就是避免这种情况为了避免这种情况
3.原因分析
程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序
4.解决方法:
方式一(synchronized关键字) :给上述代码的actor1和actor2内的代码加上synchronized,即便内部会发生重排序,但是不会影响到外部。因为加了synchronized之后只会有一个线程执行代码块中的内容
方式二(volatile):给共享变量加上volatile,可以保证不发生重排序
二.synchronized的可重入性
1.概念:
synchronized允许多层嵌套
2.代码示例:
class Mythread extends Thread{
//2.重写run方法
@Override
public void run() {
synchronized (this){
System.out.println(getName()+"进入同步代码块1");
synchronized (this){
System.out.println(getName()+"进入同步代码块2");
}
}
}
}
public class Test1 {
public static void main(String[] args) {
Mythread t1= new Mythread();
Mythread t2= new Mythread();
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
代码结果:
3.可重入的原理,好处,小结
关于synchronized的计数器:每进入一层synchronized就+1,到了结尾就-1
三.synchronized的不可中断性
1.概念:
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁, 第二个线程会一直阻塞或等待,不可被中断。
1.代码示例:
class Mythread implements Runnable{
@Override
public void run() {
synchronized (this){
System.out.println(Thread.currentThread().getName()+"进入代码块");
try {
Thread.sleep(98899);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test1 {
public static void main(String[] args) {
Mythread t= new Mythread();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
代码结果:
当线程一还在执行的时候,线程二处于阻塞状态,即便手动暂停,线程二依旧处于阻塞状态,体现了synchronized的不可中断性,线程二只能在synchronized那一行等着锁释放
四.Reentrantlock的可中断和不可中断
1.Lock可中断代码(lock.lock())
class Mythread implements Runnable{
Lock lock=new ReentrantLock();
@Override
public void run() {
lock.lock();
System.out.println(Thread.currentThread().getName()+"获得锁");
try {
sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
System.out.println("释放锁");
}
}
public class Test1 {
public static void main(String[] args) {
Mythread t= new Mythread();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("停止第线程二前");
t2.interrupt();
System.out.println("停止第线程二后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
代码结果 :
结果显示 :线程二处于WAITING状态,不可暂停
2.Lock的可中断性代码示例(trylock())
就是利用了 trylock,第二个参数是单位,比如SECOND就是秒
五.javap反汇编学习synchronized的原理
一.monitorenter
1.JVM规范文档和反汇编代码
2.monitorenter小结
synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待
二.monitorexit
1.JVM规范文档和反汇编代码
注意:从反汇编的Exception table可以看出:synchronized一旦出现异常是会释放锁的
三.同步方法
也就是说在进入方法时隐式调用monitorenter,结束的时候隐式调用monitorexit
四.小结
通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令.每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数当执行到monitorexit时,recursions会-1 ,当计数器减到0时这个线程就会释放锁
五.synchronized和lock的区别(面试题)
注1:由于synchronized是重量锁,效率比较低,虽然相较于以前进行了优化
注2:ReentranLock是否是公平锁可以自己控制,在实例化lock的时候参数不写就默认非公平,(fair)表示公平锁
ReentrantLock lock=new ReentrantLock();//非公平锁
ReentrantLock lock=new ReentrantLock();//公平锁
公平锁 :即满足队列的原则,先进先出
非公平锁 :所有进程同时抢一把锁看运气