Java锁有两种特性:并发性和可见性。
保证可见性的方法:Volatile Synchronized Final(一旦初始化完成其他线程就可见)。
一. Volatile
Volatile能够使变量在发生改变后尽快让其他线程知道。
使用Volatile的原因:
编译器为了使程序的运行速度加快,对某些变量的写操作会在cpu或寄存器上进行,最后才写入内存,而在这个过程中,变量的新值对其他线程是不可见的。
public class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("进入到run方法");
while (isRunning == true) {
}
System.out.println("线程执行完成");
}
}
public class Run {
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
分析:main线程中调用了thread.setRunning(false)想让RunThread线程结束,但如果使用JVM -server参数执行程序时,并不会结束,而是进入了死循环。
原因:现在有两个线程:main线程和RunThread线程,两个线程都想对isRunning进行更改,按照JVM内存模型,main线程将setRunning读取到本地内存并进行修改,修改后刷新回主存,而在JVM设置为-server模式运行时,RunThread会在私有堆栈中读取isRunning变量,因此无法读取到被被main线程修改后的isRunning变量值,从而就会导致RunThread线程进入死循环,而无法终止。
解决方法:volatile private boolean isRunning=true;
原理分析:当对volatile标记的变量值进行修改时,会将其他线程中存储的值清除掉,然后重新读取。一般来说,会在修改A内存中的变量值之后,清除掉其他缓存中的值,当其他缓存中的线程B需要读取此变量的值时,先向总线发出请求,然后线程A接收到请求时,会将修改后的变量值传给线程B,最后将新值写入内存。volatile的作用是每次有变量的值进行更改的时候都会重复此步骤。
二. Synchronized
当synchronized修饰某一代码块或某一方法时,同一时段最多只能有一个线程访问该代码块,当有两个并发线程同时访问Object对象中的加锁代码块时,一个时间内只能有一个线程访问该加锁区域,另一个线程只能等待,必须等到当前线程执行完毕后才能访问该加锁区域,但此时该线程可以访问非加锁区域。
synchronized方法使用:
public synchronized void Run(){
//方法体
}
synchronized的缺陷:
1.synchronized是java的一个关键字,也就是说拥有java语言内置对象的特性,当synchronized对某一代码块进行加锁时,同一时间只能有一个线程对该代码块进行访问,其他线程只能等待该线程释放锁,即便调用interrupt()方法也不会中断线程的等待(interrupt方法只能中断阻塞中的线程),只能一直等待下去,此时拥有锁的线程释放锁只有两种可能:
(1)获得锁的线程执行完毕释放锁。
(2)线程执行异常,此时JVM会让线程释放锁。
但是获得锁的线程如果要等待IO或由于其他原因(调用sleep方法等)阻塞了,而又没有释放锁,那么此时其他线程只能一直等待,从而降低程序执行效率。
2.当多个线程进行读写操作时会发生冲突,进行写写操作时也会发生冲突,但当多个线程进行读读操作时却不会发生冲突,当被synchronized修饰的代码块,即便所有线程都是进行读读操作,此时其他线程也需要等待,仍然会降低程序运行效率。
为解决以上缺陷,就会用Lock来修饰该代码块。
三. Lock
Lock是一个类,通过这个类可以实现线程的同步访问。
四. volatile和synchronized的区别
1.volatile只能修饰变量,而synchronized可以修饰变量和方法。
2.volatile不会造成线程的阻塞,而synchronized可能造成线程的阻塞。
3.volatile的本质是告诉JVM该变量的值时不确定的,只能从主存中读取,而synchronized的本质是对变量进行加锁,同一时刻只允许一个线程访问,其他线程只能等待。
4.volatile只能保证变量的修改可见性,而synchronized能保证变量的修改可见性和原子性。(《Java编程思想上》讲,定义long或double变量时,使用volatile可以获得原子性(简单的赋值操作和返回操作))。
5.当某一变量的值依赖于之前的值(如n=n+1 n++)或者当某一区域的值依赖于其他区域的值(如Range类的lower边界和upper边界必须满足lower<=upper)的时候,volatile将不在适用。
6.使用volatile的唯一安全情况是类中只有一个可变的域。
五. synchronized和lock的区别
1.synchronized是java的关键字,具有java内置语言的特性,而Lock是java中的类。
2.使用synchronized不需要手动释放锁,而使用Lock需要在finally块中调用unLock ()方法手动释放锁,如果不手动释放锁将会发生死锁现象。
3.Lock可以提高多线程读读操作的效率。
4.Lock可以调用方法中断阻塞的线程,而synchronized不可以,等待的线程只能一直等待。
5.通过Lock可以知道线程有没有获得锁,而synchronized无法办到。
在性能上讲,如果竞争资源不激烈时,Lock和synchronized的性能差不多,但当有大量线程竞争时,Lock的性能要远远优于synchronized,因此在使用时要看具体情况决定。
附:
wait(),sleep(),notify(),notifyall()讲解:
wait():释放占有的对象锁,线程进入线程池,释放cpu,此时其他线程可以抢占锁,获得锁的线程执行程序。
sleep():调用sleep()的线程会进入一段休眠时间,此时只释放cpu而不会释放对象锁,其他线程不会进入代码内部,休眠结束时,线程重新获得cpu执行代码。sleep()与wait()的最大区别在于wait()会释放对象锁而sleep()不会释放对象锁。
notify():唤醒因调用wait而等待的线程。其实就是对对象锁的的唤醒,从而使调用wait()方法的线程可以获得对象锁。调用notify并不会立即释放锁,而是继续执行synchronized代码块,直到执行完毕释放对象锁。JVM会在等待的过程中调用一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。
notifyall():唤醒所有线程。