Java 多线程(四)线程同步

    技术2025-02-16  20

    线程同步

    看一个打印车票的例子

    /** * 此程序存在线程的安全问题:打印车票时,会出现重票、错票 */ class Window1 implements Runnable { private int ticket = 100; @Override public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } else { break; } } } } public class TestWindow1 { public static void main(String[] args) { Window1 w = new Window1(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }

    经过多次测试,在打印台上能看到票号为0或者为负数的情况。

    上述代码有安全问题,需要在代码中添加同步机制

    使用synchronized来解决同步问题,有以下两种方式:

    同步代码块: synchronized (对象){ // 需要被同步的代码 } synchronized还可以放在方法声明中,表示整个方法为同步方法。 public synchronized void show (String name){ // 需要被同步的代码 }

    要想使用好同步机制,我们需要进一步理解同步机制中的锁是什么?

    同步机制中的锁

    同步锁机制: 在《Thinking in Java》中有一段解释:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。

    synchronized的锁是什么?

    任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。同步方法的锁:静态方法(类名.class)、非静态方法(this)同步代码块:自己指定,很多时候也是指定为this或类名.class

    我们需要注意是:

    必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),在同步代码块中指定锁需谨慎,以免有安全问题。

    同步的范围

    代码是否存在线程安全? (1)明确哪些代码是多线程运行的代码 (2)明确多个线程是否有共享数据 (3)明确多线程运行代码中是否有多条语句操作共享数据如何解决? 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的这些语句都要放在同步范围中

    合理使用同步锁机制: 范围太小:没锁住所有有安全问题的代码 范围太大:没发挥多线程的功能

    释放锁的操作

    当前线程的同步方法、同步代码块执行结束。当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

    不会释放锁的操作

    线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程。

    怎么使用同步锁机制

    下面我们用同步锁机制来解决上述 打印车票的安全问题

    1、实现Runnable接口,同步代码块方式

    /** * 例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式 * * 1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题 * 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。 * 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他 * 线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。 * * * 4.在Java中,我们通过同步机制,来解决线程的安全问题。 * * 方式一:同步代码块 * * synchronized(同步监视器){ * //需要被同步的代码 * * } * 说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。 * 2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。 * 3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。 * 要求:多个线程必须要共用同一把锁。 * * 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。 * 方式二:同步方法。 * 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。 * * * 5.同步的方式,解决了线程的安全问题。---好处 * 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 ---局限性 */ class Window1 implements Runnable{ private int ticket = 100; // Object obj = new Object(); // Dog dog = new Dog(); @Override public void run() { // Object obj = new Object(); while(true){ synchronized (this){//此时的this:唯一的Window1的对象 //方式二:synchronized (dog) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class WindowTest1 { public static void main(String[] args) { Window1 w = new Window1(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class Dog{ }

    2、继承Thread类,同步代码块方式

    /** * 使用同步代码块解决继承Thread类的方式的线程安全问题 * * 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式 * * 说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。 */ class Window2 extends Thread{ private static int ticket = 100; private static Object obj = new Object(); @Override public void run() { while(true){ //正确的 // synchronized (obj){ synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次 //错误的方式:this代表着t1,t2,t3三个对象 // synchronized (this){ if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class WindowTest2 { public static void main(String[] args) { Window2 t1 = new Window2(); Window2 t2 = new Window2(); Window2 t3 = new Window2(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }

    3、实现Runnable接口,同步方法方式

    /** * 使用同步方法解决实现Runnable接口的线程安全问题 * * * 关于同步方法的总结: * 1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。 * 2. 非静态的同步方法,同步监视器是:this * 静态的同步方法,同步监视器是:当前类本身 */ class Window3 implements Runnable { private int ticket = 100; @Override public void run() { while (true) { show(); } } private synchronized void show(){//同步监视器:this //synchronized (this){ if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } //} } } public class WindowTest3 { public static void main(String[] args) { Window3 w = new Window3(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }

    4、继承Thread类,同步方法方式

    /** * 使用同步方法处理继承Thread类的方式中的线程安全问题 */ class Window4 extends Thread { private static int ticket = 100; @Override public void run() { while (true) { show(); } } private static synchronized void show(){//同步监视器:Window4.class //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } } public class WindowTest4 { public static void main(String[] args) { Window4 t1 = new Window4(); Window4 t2 = new Window4(); Window4 t3 = new Window4(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }

    线程死锁

    不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁; 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

    解决方案:

    专门的算法、原则尽量减少同步资源的定义尽量避免嵌套同步

    看一个死锁的例子

    //死锁的演示 class A { public synchronized void foo(B b) { //同步监视器:A类的对象:a System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法"); // ① // try { // Thread.sleep(200); // } catch (InterruptedException ex) { // ex.printStackTrace(); // } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last方法"); // ③ b.last(); } public synchronized void last() {//同步监视器:A类的对象:a System.out.println("进入了A类的last方法内部"); } } class B { public synchronized void bar(A a) {//同步监视器:b System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar方法"); // ② // try { // Thread.sleep(200); // } catch (InterruptedException ex) { // ex.printStackTrace(); // } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法"); // ④ a.last(); } public synchronized void last() {//同步监视器:b System.out.println("进入了B类的last方法内部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主线程"); // 调用a对象的foo方法 a.foo(b); System.out.println("进入了主线程之后"); } @Override public void run() { Thread.currentThread().setName("副线程"); // 调用b对象的bar方法 b.bar(a); System.out.println("进入了副线程之后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); new Thread(dl).start(); dl.init(); } }

    测试结果:

    当前线程名: 主线程 进入了A实例的foo方法 当前线程名: 副线程 进入了B实例的bar方法 当前线程名: 主线程 企图调用B实例的last方法 当前线程名: 副线程 企图调用A实例的last方法

    看测试结果我们发现,A和B类的last()方法内部的打印都没有打印输出,可以看出死锁了。

    Lock(锁)

    从JDK 5.0开始,Java提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock(可重入锁) 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

    代码实现

    import java.util.concurrent.locks.ReentrantLock; /** * 解决线程安全问题的方式三:Lock锁 --- JDK5.0新增 * <p> * 1. 面试题:synchronized 与 Lock的异同? * 相同:二者都可以解决线程安全问题 * 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器 * Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock()) * <p> * 2.优先使用顺序: * Lock  同步代码块(已经进入了方法体,分配了相应资源)  同步方法(在方法体之外) * <p> * <p> * 面试题:如何解决线程安全问题?有几种方式 * */ class Window implements Runnable { private int ticket = 100; //1.实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { //2.调用锁定方法lock() lock.lock(); if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket); ticket--; } else { break; } } finally { //3.调用解锁方法:unlock() lock.unlock(); } } } } public class LockTest { public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }

    synchronized 与 Lock 的对比

    Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放;Lock只有代码块锁,synchronized有代码块锁和方法锁;使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类);

    优先使用顺序: Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)

    Processed: 0.010, SQL: 9