线程同步
首先看一个例子
class Shop implements Runnable {
private int ticket = 10;
@Override
public void run() {
while (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还有" + ticket-- + "张票");
}
}
}
public class Test03 {
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(shop,"A").start();
new Thread(shop,"B").start();
new Thread(shop,"C").start();
}
}
结果可见票居然出现了负数,这种问题称之为不同步。
synchronized解决的是线程之间同步问题,所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。
使用synchronized有两种方法:
- 同步代码块
- 同步方法
同步代码块代码实现
class Shop implements Runnable {
private int ticket = 10;
@Override
public void run() {
for (int i=0;i<10;i++) {
synchronized (this) {
if(ticket >=0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还有" + ticket-- + "张票");
}
}
}
}
}
public class Test03 {
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(shop,"A").start();
new Thread(shop,"B").start();
new Thread(shop,"C").start();
}
}
使用同步代码块:
synchronized(同步监视器){
//需要被同步的代码
}
需要被同步的代码:操作共享数据(该例为ticket),不能多也不能少。
同步监视器:俗称“锁”,任何一个类的对象都可用作锁,要求多个线程用同一把锁。
在实现Runable接口实现多线程,建议使用当前对象this作为同步监视器。
在继承Thread类实现多线程,建议使用当前类作为同步监视器。
同步方法代码实现
class Shop implements Runnable {
private int ticket = 10;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
this.sell();
}
}
synchronized void sell() {
if (ticket >= 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还有" + ticket-- + "张票");
}
}
}
public class Test03 {
public static void main(String[] args) {
Shop shop = new Shop();
new Thread(shop, "A").start();
new Thread(shop, "B").start();
new Thread(shop, "C").start();
}
}
同步方法:当操作共享数据的完整代码在一个方法中时,给此方法声明为同步。
synchronized void sell() {}
对于同步方法我们依然不能忽略同步监视器,只是不需要我们显式声明,当用于普通方法时同步监视器为当前类对象,当用于静态方法时则为当前类本身。
死锁
先看一个例子
public class Test {
public static void main(String[] args) {
Test test1=new Test();
Test test2=new Test();
new Thread(()->{
synchronized (test1) {
test1.show();
synchronized (test2) {
test2.show();
}
}
}).start();
new Thread(()->{
synchronized (test2) {
test2.show();
synchronized (test1) {
test1.show();
}
}
}).start();
}
public void show(){
System.out.println("我是A");
}
}
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续。
死锁的解决办法:资源上锁不要成环,不要过多的使用同步
synchronized实现原理
对象锁(monitor)机制
对于每个同步监视器,在执行同步代码块以后首先执行monitorenter指令获得同步监视器的monitor,然后程序向下执行,这一步骤是互斥的,同一时间只能有一个线程获取monitor,当线程获取monitor后,同步监视器的计数器加1,表示当前同步监视器已经有线程获取。退出的时候执行monitorexit指令,同步监视器的计数器减一直到减为0,表示该同步监视器被释放,这种计数器的好处是允许同一线程重复获取同一把锁,称为锁的可重入性。
可重入性代码:
class Reentry {
synchronized void A(){
System.out.println("我是A");
this.B();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized void B(){
System.out.println("我是B");
}
}
public class Test {
public static void main(String[] args) {
Reentry reentry=new Reentry();
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
reentry.B();
}
});
thread.start();
new Thread(new Runnable() {
@Override
public void run() {
reentry.A();
}
}).start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized优化
线程获取锁是一种”悲观锁“策略,假设每一次线程获取琐时都会遇到竞争,并且当该线程获得锁后其他竞争线程会阻塞。而CAS则是一种”乐观锁“策略,它线程访问共享资源不会遇到竞争,当然也不会阻塞其他线程的工作。
CAS操作过程:
CAS又叫做比较交换来鉴别线程是否出现冲突,当出现冲突一直重试当前操作直到没有冲突为止。
CAS(V,O,N)包含三个值,V当前内存地址存放的实际值,O预期值,N需要更改得新值。
交换过程:线程从内存中读入数据,当V值与O值相同时说明该值为最新值则把新值赋给V,当V值与O值不相同时,说明该值已经被其他线程修改所以无法把N值赋给V,此时只需要直接返回V值。并且会再次尝试交换。
CAS可能出现的问题:
ABA问题:
旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题。
自旋问题:
线程CAS失败会进行自旋,自旋会浪费大量的处理器资源,对此JVM给出的解决方案为”自适应自旋“,解决思路为,本次的自旋次数与上次CAS成功的次数做比较,从而增加或者减少本次自旋的时间。
偏向锁
当任一时刻只有同一个线程获取锁,偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程
中,持有该偏向锁的线程的加锁操作将直接返回。
轻量级锁
不同时刻只有某一个线程获取锁, 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象
原本的标记字段。
重量级锁
同一时刻有多个线程获取同一把锁, 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。