• 一 线程安全
  • 二 互斥锁之单例模式之懒汉式的线程安全问题
  • 三 线程的死锁问题
  • 四 线程通信


在之前的博客 java提升2相关章节 介绍了关于多线程的基础知识,可以翻阅查看,本章节深入了解一下多线程的应用。

一. 线程安全

对于线程安全问题,首先举个例子:

//窗口售票问题
class MyThreadDemo implements Runnable {
    int ticket = 100;

    @Override
    public void run() {
        while (true){
            if (ticket>0) {
                try {
                    Thread.currentThread().sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
            }else{
                break;
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args){
        MyThreadDemo m = new MyThreadDemo();
        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);
        Thread t3 = new Thread(m);
        t1.start();
        t2.start();
        t3.start();
    }
}

问题描述:

上面的多线程售票出现了一下错误现象

重票:

java多线程开发实战 java多线程实际开发应用_System

错票:

java多线程开发实战 java多线程实际开发应用_System_02


首先,我们分析一下为什么出现这种问题呢?

我们所期望的理想状态为:

java多线程开发实战 java多线程实际开发应用_多线程_03


但是,会出现一个极端状态:


java多线程开发实战 java多线程实际开发应用_System_04

这是由于一个线程在操作共享数据过程中,未执行完毕的情况下,例外的线程参与进来,导致了共享数据存在了安全问题,本实例的共享数据为ticket。

那么,如何处理程序的线程安全问题呢?
必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。

原理我们也理解了,那java如何去实现线程的安全呢?
使用线程的同步机制
实现方式有两种:

  • 同步代码块

synchronized(同步监视器){//需要被同步的代码块(即操作共享数据的代码)}
1. 共享数据:多个线程共同操作的同一个数据(变量)
2. 同步监视器:由任何一个类的对象来充当,哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁。

代码实现:

class MyThreadDemo implements Runnable {
    int ticket = 100;
    Object object = new Object();

    @Override
    public void run() {
        //Object object = new Object();//不行,由成员变量变为局部变量,每创建一个线程,都会创建一个对象
        while (true){
            synchronized (object){//同步监视器(锁)可以由任何一个类的对象来充当,也可以使用this,表示当前对象,只new了一次。但是在继承的方式中,一定要注意使用this,可能创建了多个线程对象,要求是多个线程使用同一个锁,即使用同一个对象。
                if (ticket>0) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
                }else{
                    break;
                }
            }
        }
    }
}

java多线程开发实战 java多线程实际开发应用_java_05

  • 同步方法

同步的方法实现在java提升2相关章节已经给出,可以前去查看。

它是将操作共享数据的方法声明为synchronized,即此方法为同步方法,能够保证当其中一个线程执行此方法时,其它线程在外等待直至此线程执行完此方法。

那么同步方法有没有锁呢,答案是有的,锁为this。但是同样要注意,在使用继承创建的线程的方式中,同样要慎用同步方法的方式,因为它的锁为this。必须保证多个线程共用一把锁。

  • 使用锁的方式(了解)

二. 互斥锁之单例模式之懒汉式的线程安全问题

互斥锁指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁为this。
- 同步方法(静态的)的锁为当前类本身。

单例模式懒汉式实现:

class Singleton {

    private Singleton() {
    }

    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

public class TestSingleton {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}
output:
true

会出现线程安全问题,多个线程同时执行时,第一个线程调用getInstance()进入if语句内,还未实例化,出现线程挂起,后面的线程也会进入会再创建一个对象,最终导致可能返回了不同Singleton对象。

解决线程安全问题实现:

public static Singleton getInstance() {
//如果第一个线程获取了单例的实例对象,后面的线程再获取实例的时候就不需要进入同步代码块了
    if (instance == null) {//多线程执行时,会先判断对象是否为null,如果为null,直接返回,无需等待进去同步代码块线程执行完毕,然后再去执行,提高了效率
        synchronized (Singleton.class) {//对于静态方法,使用但钱勒本身充当锁
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

综上,用这种方式解决了懒汉式的线程安全问题,也提高了效率,但是在实际开发中还是用饿汉式的比较多,因为这种方式,相对复杂,不适合应用。

三. 线程的死锁问题

死锁,是指不同的线程分别占用对方需要的同步资源不放弃,都再等待对方放弃自己需要的同步资源,就形成了线程的死锁。

死锁的实例:

public class TestDeadLock {

    static StringBuffer sb1 = new StringBuffer();
    static StringBuffer sb2 = new StringBuffer();

    public static void main(String[] args){

        new Thread(){
            public void run(){
                synchronized (sb1){
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("A");
                    synchronized (sb2){
                        sb2.append("B");
                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
        new Thread(){
            public void run(){
                synchronized (sb2){
                    sb1.append("C");
                    synchronized (sb1){
                        sb2.append("D");
                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
    }

}

四. 线程通信

  1. 三个方法
  • wait(): 令当前线程挂起并放弃CPU,同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
  • notify(): 唤醒正在排队等待同步资源的线程中优先级最高者结束等待
  • notifyAll(): 唤醒正在排队等待资源的所有线程结束等待。

注:Java.lang.Object提供的这三个方法只有再synchronized方法或synchronized代码块中才能使用,否则会报java.lang.lllegalMonitorStateException异常。

实例:

/**
 * 线程通信
 * 使用两个线程打印1-100,线程1,线程2,交替打印
 */

class PrintNum implements Runnable {

    int num = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this){
                notify();
                if (num <= 100) {
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                } else {
                    break;
                }


                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class TestCommunication {
    public static void main(String[] args) {
        PrintNum p = new PrintNum();
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        t1.setName("甲");
        t2.setName("乙");
        t1.start();
        t2.start();
    }
}

output:
甲:1
乙:2
甲:3
乙:4
甲:5
乙:6
...
甲:97
乙:98
甲:99
乙:100

关于多线程的相关知识,暂时先到这,后续有学习的内容,会持续更新,喜欢的,加个关注白呲牙。