16°

Java 多线程安全问题简单切入详细解析

线程安全

假如Java程序中有多个线程在同时运行,而这些线程可能会同时运行一部分的代码。如果说该Java程序每次运行的结果和单线程的运行结果是一样的,并且其他的变量值也都是和预期的结果是一样的,那么就可以说线程是安全的。

 

解析什么是线程安全:卖电影票案例

假如有一个电影院上映《葫芦娃大战奥特曼》,售票100张(1-100号),分三种情况卖票:

情况1

该电影院开设一个售票窗口,一个窗口卖一百张票,没有问题。就如同单线程程序不会出现安全问题一样。

 

情况2

该电影院开设n(n>1)个售票窗口,每个售票窗口售出指定号码的票,也不会出现问题。就如同多线程程序,没有访问共享数据,不会产生问题。

 

情况3

该电影院开设n(n>1)个售票窗口,每个售票窗口出售的票都是没有规定的(如:所有的窗口都可以出售1号票),这就会出现问题了,假如三个窗口同时在卖同一张票,或有的票已经售出,还有窗口还在出售。就如同多线程程序,访问了共享数据,会产生线程安全问题。

 

卖100张电影票Java程序实现:出现情况3类似情况

public class MovieTicket01 implements Runnable {
</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 电影票数量
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">int</span> ticketNumber = 100<span style="color: #000000;">;

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
 </span><span style="color: #008000;">*/</span><span style="color: #000000;">
@Override
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> run() {
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置此线程要执行的任务</span>
    <span style="color: #0000ff;">while</span> (ticketNumber &gt; 0<span style="color: #000000;">) {
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 提高程序安全的概率,让程序睡眠10毫秒</span>
        <span style="color: #0000ff;">try</span><span style="color: #000000;"> {
            Thread.sleep(</span>10<span style="color: #000000;">);
        } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
            e.printStackTrace();
        }
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 电影票出售</span>
        System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket01.ticketNumber + "号电影票"<span style="color: #000000;">);
        ticketNumber </span>--<span style="color: #000000;">;
    }
}

}

// 测试
public class Demo01MovieTicket {
    public static void main(String[] args) {
        // 创建一个 Runnable接口的实现类对象。
        MovieTicket01 movieTicket = new MovieTicket01();
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 创建Thread类对象,构造方法中传递Runnable接口的实现类对象(三个窗口)。</span>
    Thread window0 = <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);
    Thread window1 </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);
    Thread window2 </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);

    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置一下窗口名字,方便输出确认</span>
    window0.setName("window0"<span style="color: #000000;">);
    window1.setName(</span>"window1"<span style="color: #000000;">);
    window2.setName(</span>"window2"<span style="color: #000000;">);

    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 调用Threads类中的start方法,开启新的线程执行run方法</span>

window0.start(); window1.start(); window2.start(); } }

控制台部分输出:
售票窗口(window0)正在出售:100号电影票
售票窗口(window2)正在出售:99号电影票
售票窗口(window1)正在出售:100号电影票
售票窗口(window0)正在出售:97号电影票
售票窗口(window2)正在出售:97号电影票
售票窗口(window1)正在出售:97号电影票
售票窗口(window1)正在出售:94号电影票
售票窗口(window2)正在出售:94号电影票
.
.
.
.
.
.
售票窗口(window0)正在出售:7号电影票
售票窗口(window2)正在出售:4号电影票
售票窗口(window0)正在出售:4号电影票
售票窗口(window1)正在出售:2号电影票
售票窗口(window1)正在出售:1号电影票
售票窗口(window2)正在出售:0号电影票
售票窗口(window0)正在出售:-1号电影票

可以看到,三个窗口(线程)同时出售不指定号数的票(访问共享数据),出现了卖票重复,和出售了不存在的票号数(0、-1)

 

Java程序中为什么会出现这种情况

  1. 在CPU线程的调度分类中,Java使用的是抢占式调度。

  2. 我们开启了三个线程,3个线程一起在抢夺CPU的执行权,谁能抢到谁就可以被执行。

     

     

  3. 从输出结果可以知道,刚开始抢夺CPU执行权的时候,线程0(window0窗口)先抢到,再到线程1(window1窗口)抢到,最后线程2(window2窗口)才抢到。

     

     

  4. 那么为什么100号票已经在0号窗口出售了,在1号窗口还会出售呢?其实很简单,线程0先抢到CPU执行权,于是有了执行权后,他就开始嚣张了,作为第一个它通过while判断,很自豪的拿着ticketNumber = 100进入while里面开始执行。


  5. 可线程0是万万没有想到,这时候的线程1,在拿到执行权后,在线程0刚刚实现print语句还没开始ticketNumber --的时候,线程1以ticketNumber = 100跑进了while里面。

  6. 线程2很遗憾,在线程0执行了ticketNumber --了才急匆匆的进入while里面,不过它也不甘落后,于是拼命追赶。终于,后来居上,在线程1还没开始print的时候,他就开始print了。于是便出现了控制台的前三条输出的情况。
    售票窗口(window0)正在出售:100号电影票
    售票窗口(window2)正在出售:99号电影票
    售票窗口(window1)正在出售:100号电影票

    window0、window1、window2分别对应线程0、线程1、线程2

  7. 以此类推,直到最后程序执行完毕。

 

解决情况3的共享数据问题

通过线程的同步,来解决共享数据问题。有三种方式,分别是同步代码块、同步方法、锁机制。

同步代码块

public class MovieTicket02 implements Runnable {
</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 电影票数量
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">int</span> ticketNumber = 100<span style="color: #000000;">;


</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 创建锁对象
 </span><span style="color: #008000;">*/</span><span style="color: #000000;">
Object object </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Object();

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
 </span><span style="color: #008000;">*/</span><span style="color: #000000;">
@Override
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> run() {
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置此线程要执行的任务</span>
    <span style="color: #0000ff;">synchronized</span><span style="color: #000000;"> (object) {
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 把访问了共享数据的代码放到同步代码中</span>
        <span style="color: #0000ff;">while</span> (ticketNumber &gt; 0<span style="color: #000000;">) {
            </span><span style="color: #008000;">//</span><span style="color: #008000;"> 提高程序安全的概率,让程序睡眠10毫秒</span>
            <span style="color: #0000ff;">try</span><span style="color: #000000;"> {
                Thread.sleep(</span>10<span style="color: #000000;">);
            } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
                e.printStackTrace();
            }
            </span><span style="color: #008000;">//</span><span style="color: #008000;"> 电影票出售</span>
            System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket02.ticketNumber + "号电影票"<span style="color: #000000;">);
            ticketNumber </span>--<span style="color: #000000;">;
        }
    }
}

}

// 进行测试
public class Demo02MovieTicket {
    public static void main(String[] args) {
        // 创建一个 Runnable接口的实现类对象。
        MovieTicket02 movieTicket = new MovieTicket02();
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 创建Thread类对象,构造方法中传递Runnable接口的实现类对象(三个窗口)。</span>
    Thread window0 = <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);
    Thread window1 </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);
    Thread window2 </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);

    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置一下窗口名字,方便输出确认</span>
    window0.setName("window0"<span style="color: #000000;">);
    window1.setName(</span>"window1"<span style="color: #000000;">);
    window2.setName(</span>"window2"<span style="color: #000000;">);

    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 调用Threads类中的start方法,开启新的线程执行run方法</span>

window0.start(); window1.start(); window2.start(); } }

控制台输出:
售票窗口(window0)正在出售:100号电影票
售票窗口(window0)正在出售:99号电影票
售票窗口(window0)正在出售:98号电影票
售票窗口(window0)正在出售:97号电影票
售票窗口(window0)正在出售:96号电影票
.
.
.
.
.
.
售票窗口(window0)正在出售:5号电影票
售票窗口(window0)正在出售:4号电影票
售票窗口(window0)正在出售:3号电影票
售票窗口(window0)正在出售:2号电影票
售票窗口(window0)正在出售:1号电影票

这时候,控制台不再出售不存在的电影号数以及重复的电影号数了。

通过代码块中的锁对象,可以使用任意的对象。但是必须保证多个线程使用的锁对象是同一。锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。

总结:同步中的线程,没有执行完毕,不会释放锁,同步外的线程,没有锁,进不去同步。

 

同步方法

public class MovieTicket03 implements Runnable {
</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 电影票数量
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">int</span> ticketNumber = 100<span style="color: #000000;">;


</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 创建锁对象
 </span><span style="color: #008000;">*/</span><span style="color: #000000;">
Object object </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Object();

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
 </span><span style="color: #008000;">*/</span><span style="color: #000000;">
@Override
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> run() {
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置此线程要执行的任务</span>

ticket(); }

</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">synchronized</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> ticket() {
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 把访问了共享数据的代码放到同步代码中</span>
    <span style="color: #0000ff;">while</span> (ticketNumber &gt; 0<span style="color: #000000;">) {
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 提高程序安全的概率,让程序睡眠10毫秒</span>
        <span style="color: #0000ff;">try</span><span style="color: #000000;"> {
            Thread.sleep(</span>10<span style="color: #000000;">);
        } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
            e.printStackTrace();
        }
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 电影票出售</span>
        System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket03.ticketNumber + "号电影票"<span style="color: #000000;">);
        ticketNumber </span>--<span style="color: #000000;">;
    }
}

}

测试与同步代码块一样。

 

锁机制(Lock锁)

在Java中,Lock锁机制又称为同步锁,加锁public void lock(),释放同步锁public void unlock()。

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

public class MovieTicket05 implements Runnable {

</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 电影票数量
 </span><span style="color: #008000;">*/</span>
<span style="color: #0000ff;">private</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">int</span> ticketNumber = 100<span style="color: #000000;">;

Lock reentrantLock </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> ReentrantLock();


</span><span style="color: #008000;">/**</span><span style="color: #008000;">
 * 在实现类中重写Runnable接口的run方法,并设置此线程要执行的任务
 </span><span style="color: #008000;">*/</span><span style="color: #000000;">
@Override
</span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> run() {
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置此线程要执行的任务</span>
    <span style="color: #0000ff;">while</span> (ticketNumber &gt; 0<span style="color: #000000;">) {
        reentrantLock.lock();
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 提高程序安全的概率,让程序睡眠10毫秒</span>
        <span style="color: #0000ff;">try</span><span style="color: #000000;"> {
            Thread.sleep(</span>10<span style="color: #000000;">);
            </span><span style="color: #008000;">//</span><span style="color: #008000;"> 电影票出售</span>
            System.out.println("售票窗口(" + Thread.currentThread().getName() + ")正在出售:" + MovieTicket05.ticketNumber + "号电影票"<span style="color: #000000;">);
            ticketNumber </span>--<span style="color: #000000;">;
        } </span><span style="color: #0000ff;">catch</span><span style="color: #000000;"> (InterruptedException e) {
            e.printStackTrace();
        } </span><span style="color: #0000ff;">finally</span><span style="color: #000000;"> {
            reentrantLock.unlock();
        }
    }
}

}

// 测试

public class Demo05MovieTicket { public static void main(String[] args) { // 创建一个 Runnable接口的实现类对象。 MovieTicket05 movieTicket = new MovieTicket05();

    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 创建Thread类对象,构造方法中传递Runnable接口的实现类对象(三个窗口)。</span>
    Thread window0 = <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);
    Thread window1 </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);
    Thread window2 </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Thread(movieTicket);
    
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 设置一下窗口名字,方便输出确认</span>
    window0.setName("window0"<span style="color: #000000;">);
    window1.setName(</span>"window1"<span style="color: #000000;">);
    window2.setName(</span>"window2"<span style="color: #000000;">);

    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 调用Threads类中的start方法,开启新的线程执行run方法</span>

window0.start(); window1.start(); window2.start(); } }

控制台部分输出:
售票窗口(window0)正在出售:100号电影票
售票窗口(window0)正在出售:99号电影票
售票窗口(window0)正在出售:98号电影票
售票窗口(window0)正在出售:97号电影票
售票窗口(window0)正在出售:96号电影票
.
.
.
.
.
.
售票窗口(window1)正在出售:7号电影票
售票窗口(window1)正在出售:6号电影票
售票窗口(window1)正在出售:5号电影票
售票窗口(window1)正在出售:4号电影票
售票窗口(window1)正在出售:3号电影票
售票窗口(window2)正在出售:2号电影票
售票窗口(window1)正在出售:1号电影票

与前两种方式不同,前两种方式,只有线程0能够进入同步机制执行代码,Lock锁机制,三个线程都可以进行执行,通过Lock锁机制来解决共享数据问题。

 

Java 多线程安全问题就到这里了,如果有什么不足、错误的地方,希望大佬们指正。

本文转载自博客园,原文链接:https://www.cnblogs.com/liyihua/p/12210893.html

全部评论: 0

    我有话说: