Java多线程大闯关–线程的同步机制

  • Post author:
  • Post category:java


这个系列的博客主要是对Java高级编程中多线程相关的知识点做一个梳理,内容主要包括程序、进程、线程、并行、并发等相关概念的理解,创建多线程的几种方式,Thread类,线程的同步机制,线程通信等几个大的模块。该系列博客的文章如下,推荐按顺序阅读:



线程安全问题

多个线程执行地不确定性引起执行结果的不稳定,通俗点来说,当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来,这样就会导致共享数据的错误。这里模拟火车站售票程序,开启三个窗口售票,来看一下有些什么线程安全问题。

package com.learnjiawa.mutithread;

/**
 *
 * 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
 *
 * 存在线程的安全问题,待解决。
 *
 * @author shkstart
 * @create 2019-02-13 下午 4:20
 */
class Window extends Thread{


    private static int ticket = 100;
    @Override
    public void run() {

        while(true){

            if(ticket > 0){
                //睡一会,增加线程安全问题出现的机率
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }

        }

    }
}


public class WindowTest {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        
        t1.start();
        t2.start();
        t3.start();

    }
}

上面用继承Thread类的方式创建三个多线程,模拟窗口卖票,发现会出现重票、错票的现象。

错票重票



同步机制

Java对于多线程的安全问题提供了专业的解决方式:同步机制。对多条操作共共享数据的语句,只能让一个线程执行完,在执行过程中,其他线程不可以参与执行。

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

synchronized的锁是什么?

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

需要注意的是:必须确保使用同一个资源的

多个线程共用一把锁

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



同步的范围

如何找问题,即代码是否存在线程安全?

  1. 明确哪些代码是多线程运行的代码
  2. 明确多个线程是否有共享数据
  3. 明确多线程运行代码中是否有多条语句操作共享数据

如何解决问题?

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其它线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中。切记同步范围如果太小,没锁住所有安全问题的代码就会出问题;同步范围如果太大,就不能够发挥多线程的功能额



方式一:同步代码块

synchronized(对象){


//需要被同步的代码;

}

package com.learnjiawa.mutithread;

/**
 * @author shkstart
 * @create 2019-02-15 上午 11:15
 */
/**
 * 使用同步代码块解决继承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();
    }
}

需要说明的是:

  1. 操作共享数据的代码,即为需要被同步的代码。 –>不能包含代码多了,也不能包含代码少了。
  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
  3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
  4. 要求:多个线程必须要共用同一把锁。
  5. 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。 在继承

同步的方式,解决了线程的安全问题。但是在操作同步代码时,只能一个线程参与,其他线程需要等待,相当于是一个单线程的过程,效率低。



方式二:同步方法

public synchronized void show(String name){


//需要被同步的代码

}

package com.learnjiawa.mutithread;
/**
 * 使用同步方法解决实现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();
    }

}



方式三:Lock锁
  • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
//如果同步代码有异常,要将unlock()写入finally语句块
class A{
	private final ReentrantLock lock = new ReentrantLock();
	public void m(){
		lock.lock();
		try{
				//保证线程安全的代码
		}finally{
				lock.unlock();
		}
	}
}
package com.learnjiawa.mutithread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 解决线程安全问题的方式三:Lock锁  --- JDK5.0新增
 * 1. 面试题:synchronized 与 Lock的异同?
 *   相同:二者都可以解决线程安全问题
 *   不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
 *        Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
 * 2.优先使用顺序:
 * Lock同步代码块(已经进入了方法体,分配了相应资源) 同步方法(在方法体之外)
 *  面试题:如何解决线程安全问题?有几种方式
 */
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();
    }
}



线程安全的单例模式(懒汉式)

package com.learnjiawa.mutithread;

/**
 * 使用同步机制将单例模式中的懒汉式改写为线程安全的
 */
public class BankTest {

}

class Bank{

    private Bank(){}

    private static Bank instance = null;

    public static Bank getInstance(){
        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//
//                instance = new Bank();
//            }
//            return instance;
//        }
        //方式二:效率更高
        if(instance == null){

            synchronized (Bank.class) {
                if(instance == null){

                    instance = new Bank();
                }

            }
        }
        return instance;
    }

}



线程的死锁问题

死锁可以理解为,不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

  • 出现死锁后,不会出现异常,不会出现提示,只是所有线程都处于阻塞状态,无法继续
  • 使用同步时,要避免出现死锁。

死锁问题代码演示:

package com.learnjiawa.mutithread;
//死锁的演示
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("进入了主线程之后");
	}
	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();
	}
}



参考文献

[1]Bruce Eckel.Java编程思想(第4版)[M].机械工业出版社,2008:650-757.



更多

对我的文章感兴趣,持续更新中…



版权声明:本文为qq_31994231原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。