解决线程不安全问题

Posted yunqing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解决线程不安全问题相关的知识,希望对你有一定的参考价值。

当多个线程并发访问同一个资源对象时,可能会出现线程不安全的问题,比如现有100个高铁座位,现在有请三个窗口(A,B,C)同时售票.,此时使用多线程技术来实现这个案例.

package com.yunqing.ssm.test;

/**
 * 存在线程安全问题
 */
public class TestRunnable {

    public static void main(String[] args) {
        Ticket tick = new Ticket();

        Thread t1 = new Thread(tick,"A窗口");
        Thread t2 = new Thread(tick,"B窗口");
        Thread t3 = new Thread(tick,"C窗口");

        t1.start();
        t2.start();
        t3.start();
    }



}

class Ticket implements Runnable{

    int ticket = 100;

    @Override
    public void run() {

        while (true){
            
            try {
                //模拟网络延迟
                Thread.currentThread().sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
                if (ticket>0)
                    System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);

        }

    }

}

以上代码运行结果:

技术分享图片

 

为什么编号为84的座位号被3个窗口售出了?

当A窗口打印84座位号,还没打印完的时候,其他两个线程就也进入到了84号座位票的分配操作中,所以导致线程安全问题。

要解决上述多线程并发访问多一个资源的安全性问题,就必须得保证打印座位号和座位号总数减1操作,必须同步完成.即是说,A线程进入操作的时候,BC线程只能在外等着,A操作结束,ABC才有机会进入代码去执行.

解决多线程并发访问资源的安全问题,有三种方式:

方式1:同步代码块

方式2:同步方法

方式3:锁机制(Lock)

 

 

方式1:同步代码块

语法:

synchronized(同步锁)

{

     需要同步操作的代码

}

 

同步锁:

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.也称为同步监听对象/同步锁/同步监听器/互斥锁。

实际上,对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,谁拿到锁,谁就可以进入代码块,其他线程只能在代码块外面等着,而且注意,在任何时候,最多允许一个线程拥有同步锁.

Java程序运行可以使用任何对象作为同步监听对象,但是一般的,我们把当前并发访问的共同资源作为同步监听对象.

package com.yunqing.ssm.test;

/**
 * 存在线程安全问题
 */
public class TestRunnable {

    public static void main(String[] args) {
        Ticket tick = new Ticket();

        Thread t1 = new Thread(tick,"A窗口");
        Thread t2 = new Thread(tick,"B窗口");
        Thread t3 = new Thread(tick,"C窗口");

        t1.start();
        t2.start();
        t3.start();
    }



}

class Ticket implements Runnable{

    int ticket = 100;

    @Override
    public void run() {

        while (true){

            try {
                //模拟网络延迟
                Thread.currentThread().sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //this代表Ticket对象,Ticket对象是多线程共享资源
            synchronized (this){
                if (ticket>0)
                    System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
            }


        }

    }

}

  方式2:同步方法:

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着.

Synchronized public void doWork(){

     ///TODO

}

同步锁是谁:

      对于非static方法,同步锁就是this.  

      对于static方法,我们使用当前方法所在类的字节码对象(Ticket.class).

package com.yunqing.ssm.test;

/**
* 存在线程安全问题
*/
public class TestRunnable {

public static void main(String[] args) {
Runnable a = new Ticket();
new Thread(a,"A窗口").start();
new Thread(a,"B窗口").start();
new Thread(a,"C窗口").start();

}



}

class Ticket implements Runnable{

int ticket = 100;

synchronized private void doWork() throws InterruptedException {

if (ticket>0){
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
Thread.sleep(100);
}

}

@Override
public void run() {

for (int i=1;i<=100;i++){
try {
doWork();
} catch (InterruptedException e) {
e.printStackTrace();
}
}



}

}

注意:

不要使用synchronized修饰run方法,修饰之后,某一个线程就执行完了所有的功能好比是多个线程出现串行.

 

解决方案:把需要同步操作的代码定义在一个新的方法中,并且该方法使用synchronized修饰,再在run方法中调用该新的方法即可.

 

实际上,同步代码块和同步方法差不了多少,在本质上是一样的,两者都用了一个关键字synchronizedsynchronized保证了多线程并发访问时的同步操作,避免线程的安全性问题,但是有一个弊端,就是使用synchronized的方法/代码块的性能比不用要低一些,因此如果要用synchronized,建议尽量减小synchronized的作用域。

 

方式3:同步锁(锁机制)

 

Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象.


package com.yunqing.ssm.test;

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

/**
* 存在线程安全问题
*/
public class TestRunnable {

public static void main(String[] args) {
Runnable a = new Ticket();
new Thread(a,"A窗口").start();
new Thread(a,"B窗口").start();
new Thread(a,"C窗口").start();

}



}

class Ticket implements Runnable{

int ticket = 100;

//创建锁对象
private final Lock lock = new ReentrantLock();

private void doWork(){
//进入方法,立马加锁
lock.lock();


try {

if (ticket>0){
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
Thread.sleep(100);
}


} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}


}

@Override
public void run() {

for (int i=1;i<=100;i++){
doWork();
}



}

}

 

以上是关于解决线程不安全问题的主要内容,如果未能解决你的问题,请参考以下文章

为啥基于锁的程序不能组成正确的线程安全片段?

应用程序启动器 “sublime_text.desktop“ 还没有被标记为 信任。如果您不知道这个文件的来源,那么启动它可能会不安全。解决sublime在ubuntu中不支持中文输入问题。(代码片段

Java线程 — 线程同步及安全问题

markdown 线程安全相关片段

SimpleDateFormat线程不安全的5种解决方案!

Java并发多线程编程——集合类线程不安全之HashMap的示例及解决方案