Java 多线程同步的两大方式

1. 同步问题的引出
先看一段代码,模拟多个线程卖票:

class MyThread implements Runnable {
    private int ticket = 10 ; // 一共十张票
    @Override
    public void run() {
        while(this.ticket>0) { // 还有票
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } // 模拟网络延迟
            System.out.println(Thread.currentThread().getName()+
                    ",还有" +this.ticket -- +" 张票");
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        MyThread mt = new MyThread() ;
        new Thread(mt,"黄牛A").start();
        new Thread(mt,"黄牛B").start();
        new Thread(mt,"黄牛C").start();
    }
}

运行结果:
这个时候我们发现,票数竟然出现负数,这种问题我们称之为不同步操作。
不同步的唯一好处是处理速度快(多个线程并发执行)

2. 同步处理
所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。
2.1 synchronized处理同步问题
如果要想实现这把”锁”的功能,可以采用关键字synchronized来处理。
使用synchronized关键字处理有两种模式:同步代码块、同步方法
使用同步代码块 :如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象:this

首先看同步代码块:

class MyThread implements Runnable {
    private int ticket = 10 ; // 一共十张票
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 在同一时刻,只允许一个线程进入代码块处理
            synchronized(this) { // 表示为程序逻辑上锁
                if(this.ticket>0) { // 还有票
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } // 模拟网络延迟
                    System.out.println(Thread.currentThread().getName()+",还有" +this.ticket --
                            +" 张票");
                }
            }
        }
    }
}

public class TestDemo {
    public static void main(String[] args) {
        MyThread mt = new MyThread() ;
        Thread t1 = new Thread(mt,"黄牛A");
        Thread t2 = new Thread(mt,"黄牛B");
        Thread t3 = new Thread(mt,"黄牛C");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t3.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}

程序运行结果:
这一种方式是在方法里拦截的,也就是说进入到方法中的线程依然可能会有多个。

同步方法:

class MyThread implements Runnable {
    private int ticket = 100 ; // 一共一百张票
    @Override
    public void run() {
        for (int i = 0; i < 100; 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 TestDemo {
    public static void main(String[] args) {
        MyThread mt = new MyThread() ;
        Thread t1 = new Thread(mt,"黄牛A");
        Thread t2 = new Thread(mt,"黄牛B");
        Thread t3 = new Thread(mt,"黄牛C");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t3.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}

程序运行结果:
同步虽然可以保证数据的完整性(线程安全操作),但是其执行的速度会很慢。

2.2 关于synchronized的额外说明
先看一段代码:

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();
        }
    }
}

通过上述代码以及运行结果我们可以发现,没有看到synchronized起到作用,三个线程同时运行test()方法。
实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。

当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。

那么,如果真要锁住这段代码,要怎么做?
这里有两种思路:
(1)锁住同一个对象

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 MyThread extends Thread {
    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();
        for (int i = 0; i < 3 ; i++) {
            Thread thread = new MyThread(sync) ;
            thread.start();
        }
    }
}

(2)让synchronized锁这个类对应的Class对象

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 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(sync) ;
            thread.start();
        }
    }
}

上面代码用synchronized(Sync.class)实现了全局锁的效果。因此,如果要想锁的是代码段,锁住多个对象的同一方法,使用这种全局锁,锁的是类而不是this。
static synchronized方法,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的Class对象,所以,static synchronized方法也相当于全局锁,相当于锁住了代码段。

3. JDK 1.5 提供的Lock锁
范例:使用ReentrantLock进行同步处理:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyThread implements Runnable {
    private int ticket = 500;
    private Lock ticketLock = new ReentrantLock() ;
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            ticketLock.lock();
            try {
                if (this.ticket > 0) { // 还有票
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } // 模拟网络延迟
                    System.out.println(Thread.currentThread().getName() 
                                + ",还有"+ this.ticket-- + " 张票");
                }
            } finally {
                ticketLock.unlock();
            }
        }
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt, "黄牛A");
        Thread t2 = new Thread(mt, "黄牛B");
        Thread t3 = new Thread(mt, "黄牛C");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t3.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。
到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

猜你喜欢

转载自blog.csdn.net/yubujian_l/article/details/80204058