synchronized处理同步问题
JAVA中synchronized关键字用来处理多线程的同步问题,解决每一个线程对象轮番强占资源带来的问题。
它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。
synchronized关键字的使用
使用synchronized关键字处理有两种模式:同步代码块、同步方法。
首先看同步代码块模式:
(1)必须设置一个要锁定的对象,一般可以锁定当前对象:this。
//对静态代码块加synchronized
public class TestTickThread {
public static void main(String[] args) {
Runnable runnable = new MyTick2Runnable();
new Thread(runnable, "Thread-黄牛A").start();
new Thread(runnable, "Thread-黄牛B").start();
new Thread(runnable, "Thread-黄牛C").start();
}
}
class MyTickRunnable implements Runnable {
private int tick = 10;
public void run() {
while (this.tick > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) {
if (this.tick > 0) {
System.out.println(Thread.currentThread().getName() + " 买票, 剩余 " + (--this.tick));
}
}
}
}
}
(2)使用同步方法:
//对一个类的方法加synchronized
public class testTick2 {
public static void main(String[] args){
Runnable runnable=new MyTick2();
new Thread(runnable,"Thread-黄牛A").start();
new Thread(runnable,"Thread-黄牛B").start();
new Thread(runnable,"Thread-黄牛C").start();
}
}
class MyTick2 implements Runnable{
private int tick=10;
public void run(){
while(this.tick>0){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
this.sale();
}
}
public synchronized void sale(){
if(this.tick>0){
System.out.println(Thread.currentThread().getName()+"买票,剩余"+(--this.tick));
}
}
}
synchronized关键字的额外说明
对于下面的代码,synchronized用于锁住多个对象,并未锁住它同一对象的同步代码段,所以并未实现线程同步的结果。
public class testSycThread{
public static void main(String[] args) {
//通过lock锁锁方法同一对象
for (int i = 0; i < 3; i++) {
Thread thread = new MySyncThread1();
thread.start();
}
}
}
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 MySyncThread1 extends Thread{
public void run(){
Sync sync=new Sync();
sync.test();
}
}
synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。
对于synchronized实现锁住一个对象的同步代码段,改进方法如下:
(1)synchronized锁住多个线程的同一对象
public class testSycThread{
public static void main(String[] args) {
//通过lock锁锁方法同一对象
Sync sync = new Sync();
for (int i = 0; i < 3; i++) {
Thread thread = new MySyncThread2(sync);
thread.start();
}
}
}
class Sync{
public void test() {
synchronized (this) {
System.out.println("test执行开始,当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test执行结束,当前线程" + Thread.currentThread().getName());
}
}
}
class MySyncThread2 extends Thread {
private Sync sync ;
public MySyncThread2(Sync sync) {
this.sync = sync ;
}
public void run() {
sync.test();
}
}
(2)synchronized锁这个类对应的Class对象(全局锁):
public class testSycThread{
public static void main(String[] args) {
//通过lock锁锁方法同一对象
Sync sync = new Sync();
for (int i = 0; i < 3; i++) {
Thread thread = new MySyncThread2(sync);
thread.start();
}
}
}
class Sync{
public 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 MySyncThread1 extends Thread{
public void run(){
Sync sync=new Sync();
sync.test();
}
}
synchronized优化
Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。
JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。在JDK1.6中,提供了很多的synchronized方法,这里将主要介绍CAS和Java头两种优化方法。
1.CAS优化
CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞。
无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
CAS操作过程如下图:
CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。
CAS的问题:
(1)ABA问题
如下图:
CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。
解决方案:可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题。
(2)自旋转问题
转自旋会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它期望在运行无用指令的过程中,锁能够被释放出来。
解决方案:自适应自旋,即如果自旋转单位时间n内(即旋转的次数)获得到锁,下次自选时间边长些,如果自旋转单位时间n内(即旋转的次数)未获得到锁,下次自选时间边短。
(3)公平性问题
当处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。
解决方案:使用ock体系可以实现公平锁。
- Java对象头
对象的锁可以理解为类似对对象的一个标志,这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
四种对象锁的MarkWord变化为下图:
对于四种对象锁的升级关系图如下:
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种:
(1) 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
(2) 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
(3)偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。