目录
一.同步问题
1.synchronized(内建锁)实现同步处理
实现锁的功能,可以使用synchronized关键字。
使用synchronized关键字处理有两种模式:同步代码块、同步方法。
同步代码块:
使用同步代码块必须设置一个要锁定的对象,一般可以锁定当前对象:this。
注意:同一时刻只有一个线程可以进入同步代码块,但是可以有多个线程进入方法。
package www.like.java;
class MyThread implements Runnable{
private int ticket = 100;
@Override
public void run() {
for (int i = 0; i < 100; i++){
synchronized (this){
if (this.ticket > 0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"还剩下"+this.ticket--+"票");
}
}
}
}
}
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"售票人1");
Thread thread2 = new Thread(myThread,"售票人2");
Thread thread3 = new Thread(myThread,"售票人3");
thread1.start();
thread2.start();
thread3.start();
}
}
分析:
三个线程是在同时卖票的,但是在同一时刻,一定只有一个线程在卖票(上锁了,其他线程进不去,在该线程卖出一张票后,解锁,其他的线程才可以进入),而且票数是递减的。
同步方法:
在方法声明上加上synchronized关键字,表示此时只有一个线程可以进入同步方法。
package www.like.java;
class MyThread implements Runnable{
private int ticket = 100;
@Override
public void run() {
for(int i = 0; i < 1000; i++){
this.sale();
}
}
public synchronized void sale(){
if (this.ticket > 0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"还剩下"+this.ticket--+"票");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"售票人1");
Thread thread2 = new Thread(myThread,"售票人2");
Thread thread3 = new Thread(myThread,"售票人3");
thread1.start();
thread2.start();
thread3.start();
}
}
同步虽然可以保证数据的完整性(线程安全操作),但是其执行速度会很慢。
2.synchronized对象锁概念
我们先来看一段代码:
package www.like.java;
class Sync {
public synchronized void test(){
System.out.println("test方法开始,当前线程为"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为"+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class Test {
public static void main(String[] args) {
for(int i = 0; i < 3; i++){
Thread thread = new MyThread();
thread.start();
}
}
}
它的运行结果是这样的:
看到这里有的人可能会很奇怪,test()方法不是已经用synchronized修饰了吗,为什么在进入一个线程之后,其他的线程还可以进入呢?
synchronized(this)以及普通的synchronized方法,只能防止多个线程同时执行同一个对象的同步段。
synchronized锁的是括号中的对象而非代码。
再看一下上面的代码,三个线程有三个sync对象,相当于三个线程各上各的锁,互不干扰,所以同时进入test()方法,又同时出来。
但是如果一定要锁住这段代码该怎么办呢?
第一种思路:只要锁住统一个对象就可以了。
package www.like.java;
class Sync {
public synchronized void test(){
System.out.println("test方法开始,当前线程为"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为"+Thread.currentThread().getName());
}
}
class MyThread implements Runnable{
private Sync sync;
public MyThread(Sync sync) {
this.sync = sync;
}
@Override
public void run() {
this.sync.test();
}
}
public class Test {
public static void main(String[] args) {
Sync sync = new Sync();
MyThread myThread = new MyThread(sync);
for(int i = 0; i < 3; i++){
Thread thread = new Thread(myThread,"线程"+i);
thread.start();
}
}
}
看一下结果:
第二种思路:让synchronized锁这个类对应的class对象。(class对象是唯一的)
全局锁:
package www.like.java;
class Sync {
public synchronized void test(){
synchronized (Sync.class){
System.out.println("test方法开始,当前线程为"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为"+Thread.currentThread().getName());
}
}
}
class MyThread extends Thread{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class Test {
public static void main(String[] args) {
for(int i = 0; i < 3; i++){
Thread thread = new MyThread();
thread.start();
}
}
}
看一下结果:
全局锁:锁代码段
- 使用类的静态同步方法:synchronized与class共同使用,此时锁的是当前使用的类而非对象。
- 在同步代码段中锁当前的class对象:synchronized(类名称.class){}
3.synchronized底层实现
同步代码块底层实现:
执行同步代码块首先要执行monitorenter指令,退出时执行monitorexit指令。
使用synchronized实现同步,关键点就是要获取对象的监视器monitor对象。当线程获取到monitor对象后,才可以执行同步代码块,否则就只能等待。同一时刻只有一个线程可以获取到该对象的monitor监视器。
通常一个monitor指令会同时包含多个monitorexit指令。因为JVM要确保所获取的锁无论在正常路径还是异常路径都能正确解锁。
同步方法底层实现:
当使用synchronized标记方法时,字节码会出现一个访问标记:ACC_SYNCHRONIZED。该标记表示在进入该方法时,JVM需要进行monitorenter操作。在退出该方法时,无论是否正常返回,JVM均需要进行monitorexit操作。
当JVM执行monitorenter时,如果目标对象monitor的计数器为0,表示此时该对象没有被其它线程所持有。此时JVM会将该锁对象的持有线程设置为当前线程,并且将monitor计数器加1。
在目标对象的计数器不为0的情况下,如果锁的持有线程时当前线程,JVM可以将计数器再次加1(可重入锁);否则需要等待,直到持有线程释放该锁。
对象锁(monitor)机制是JVM1.6之前synchronized底层原理,又称为JDK1.6重量级锁,线程的阻塞以及唤醒均需要操作系统由用户态切换到内核态,开销非常大,因此效率很低。
二.synchronized(内建锁)优化
1.CAS(Campare and Swap)
悲观锁:线程获取锁(JDK1.6之前内建锁)是一种悲观锁的策略。假设每一次执行临界区代码(访问共享资源)都会产生冲突,所以当前线程获取到锁的同时也会阻塞其他未获取到锁的线程。
乐观锁(CAS操作,无锁操作):假设所有线程访问共享资源时不会产生冲突,由于不会产生冲突自然就不会阻塞其他线程。因此线程就不会存在阻塞停顿的状态。出现冲突时,无锁操作使用CAS(比较交换)来鉴别线程是否出现冲突,出现冲突就重试当前操作,直到没有冲突为止。
1.1 CAS操作过程
CAS可以通俗的理解为CAS(V,O,N):
V:当前内存地址实际存放的值
O:预期值(旧值)
N:更新的新值
当 V = O 时,期望值与内存值相等,该值没有被其它任何线程修改过,即O就是目前最新的值,因此可以将新值N赋给V。反之如果 V != O ,表明该值已经被其它线程修改过了,因此O值并不是当前最新值,返回V,无法修改。
当多个线程使用CAS操作时,只有一个线程会成功,其余线程均失败,失败的线程会重新尝试(自旋)或挂起线程(阻塞)。
内建锁优化前的最大问题在于:在存在线程竞争的情况下会出现线程的阻塞以及唤醒带来的性能问题,这是一种互斥同步(阻塞同步)。
(获取不到锁会将线程挂起,等待释放后再获取,开销很大)
而CAS不是将线程挂起,当CAS失败后会进行一定的尝试操作并非耗时的将线程挂起,也叫非阻塞同步。
(获取不到锁会将线程阻塞,一直不停的尝试获取)
1.2 CAS的问题
- ABA问题:解决方法--->使用atomic包提供的AtomicStampedReference类来解决
- 自旋会浪费大量的CPU资源:解决方法--->自适应自旋
- 公平性
2. Java对象头
在同步的时候获取对象的monitor,即获取到对象的锁。那么,对象的锁怎么理解呢?无非就是类似于对象的一个标志,这个标志就是存放Java对象的对象头。
32位JVM Mark Word默认存储结构为:
根据竞争状态的激烈程度,锁会自动升级,锁不能降级(为了提高锁获取与释放的效率)。
对象的Mark Word变化入下图:
四种状态锁:
无锁 0 01
偏向锁 1 01
轻量级锁 00
重量级锁 10
3.偏向锁
大多数情况下,锁不仅不存在线程竞争,而且总是由同一个线程多次获得。为了让线程获取锁的开销降低引入偏向锁。
偏向锁是锁状态中最乐观的一种锁:从始至终只有一个线程请求一把锁。
偏向锁只有一次CAS过程,出现在加锁时。
偏向锁的获取:
当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段存储锁偏向的线程ID,以后该进程在进入和退出同步块时不需要进行CAS操作来进行加锁和解锁,直接进入。
当线程访问同步块失败时(已经有线程进入),使用CAS竞争锁,并将偏向锁升级为轻量级锁。
偏向锁用了一种等待竞争出现才释放锁的机制。所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)。如果持有线程已经终止,则将锁对象的对象头设置为无锁状态。
4.轻量级锁
多个线程在不同的时间段请求同一把锁,也就是不存在锁竞争的情况。针对这种情况,JVM采用了轻量级锁来避免线程的阻塞与唤醒。
轻量级锁不停的CAS。
当多个线程同时竞争同一把锁时,升级为重量级锁。
5.重量级锁
重量级锁就是monitor实现。
总结:
- 偏向锁只会在第一次请求锁时采用CAS操作,并在锁对象的标记字段记录下当前线程地址。在此后的线程运行过程中,持有偏向锁的线程无需加锁操作。(针对的是锁仅会被统一线程持有的状况)
- 轻量级锁采用CAS操作,将锁对象标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。(针对的是多个线程在不同时间段申请同一把锁的情况)
- 重量级锁会阻塞、唤醒请求加锁的线程。(针对的是多个线程同时竞争同一把锁的情况)JVM采用自适应自旋,来避免面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。
6. 其它优化
锁粗化
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁。
锁解除
删除不必要的加锁操作。根据代码逃逸技术,如果判断一段代码中,堆上的数据不会逃逸出当前线程,则认为此代码是线程安全的,无需加锁。