Java面试题之Thread线程安全(一)

一、概述

什么是线程安全/不安全?

如果只有一个线程才可以操作此数据,则必是线程安全。如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步(加锁)机制,会存在线程安全问题。

二、举例

假如现在A、B二人同时进行车票购买,总共有100张车票,A、B各买一张票。如果是线程安全,那么应该是A、B各买一张票后,剩余 98 张票。

线程安全:指多个线程在执行同一段代码的时候 采用加锁机制 ,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。

即A、B各买一张票,剩余98张票,数据正确。

线程不安全:指不考虑同步机制(synchronized等),有可能出现多个线程先后更改数据,造成所得到的数据是脏数据 (错误的)。

即如果是线程不安全,则可能会出现A、B各买一张票后,同时执行 100 - 1 操作 ,但剩余 99 张票,数据错误的情况。

即是否提供 加锁(同步)机制 ,是否存在数据 二义性 。线程不安全不代表数据一定会出问题,仅仅是可能出现脏数据问题,线程不安全 != 代码不安全

三、代码实例 

1、不安全的买票
package xu.com.threads.chap1;

public class Tiket implements Runnable {

    static int tiket = 10;
    boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    public void buy() {
        //模拟买票
        if (tiket <= 0) {
            flag = false;
            return;
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) { //尝试解除阻塞异常
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "抢到了第" + tiket-- + "张票!");
    }

    public static void main(String[] args) {
        Tiket tiket1 = new Tiket();
        new Thread(tiket1, "黄牛").start();
        new Thread(tiket1, "老师").start();
        new Thread(tiket1, "我").start();
    }
}

2、不安全的银行(两个人取钱) 
package xu.com.threads.chap1;

public class UnsafeBank {

    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");
        Draw boy = new Draw(account, 50, "boy");
        Draw girl = new Draw(account, 100, "girl");
        boy.start();
        girl.start();
    }

}

class Account {
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

class Draw extends Thread {
    Account account;//账户
    int drawingMoney; //取了多少钱
    int nowMoney; //现在手里有多少钱
    String name;

    public Draw(Account account, int drawingMoney, String name) { //现在账户余额,取多少钱,手里有多少钱
        this.account = account;
        this.drawingMoney = drawingMoney;
        this.name = name;
    }

    @Override
    public void run() {

        if (this.account.money - this.drawingMoney < 0) {
            System.out.println(this.name + ",您的余额不足");
            return;
        }
        try {
            Thread.sleep(3000);    //sleep放大问题的发生性,查缺补漏
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.account.money -= this.drawingMoney;//找到account堆内存地址,改变银行余额
        this.nowMoney += this.drawingMoney;
        System.out.println(this.name + "取到了" + this.nowMoney);
    }
}

没有取钱之前银行的余额为100,两人去取钱,由于线程没有同步,导致,两人取的钱总共超出了银行的余额.。

3、 不安全的集合
package xu.com.threads.chap1;

import java.util.ArrayList;

public class UnsafeList {

    public static void main(String[] args) throws InterruptedException {

        ArrayList<String> list = new ArrayList<>();     //ArrayList集合线程不安全演示
        for (int i = 1; i < 10000; i++) {

            new Thread(
                    () -> {         //lamda表达式
                        list.add(Thread.currentThread().getName());
                    }
            ).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }
}

最后结果永远不会是10000个,因为线程不安全,有些数据会被别的线程覆盖掉 

四、优化实例

线程同步有三种方式:

  • 同步代码块
  • 同步方法
  • 锁机制
1、同步代码块: 

synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问

package xu.com.threads.chap1;

public class TicketSync implements Runnable {

    // 总共一百张票
    private int ticket = 10;

    // 创建锁对象
    Object lock = new Object();

    /*
     * 执行卖票操作
     * */
    @Override
    public void run() {
        // 窗口永远卖票
        while (true) {
            // 同步代码块
            synchronized (lock) {
                if (ticket > 0) {
                    // 这里把sleep去掉运行结果更明显
                    // 获取当前线程的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在卖: " + ticket--);
                }
            }

        }
    }

    public static void main(String[] args) {

        TicketSync ticket = new TicketSync();
        Thread t1 = new Thread(ticket, "窗口一");
        Thread t2 = new Thread(ticket, "窗口二");
        Thread t3 = new Thread(ticket, "窗口三");

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

}

2、同步方法:

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

//  同步方法格式
public synchronized void method(){

 	可能会产生线程安全问题的代码 
 }
package xu.com.threads.chap1;

/**
 * @Description
 * @auther 宁宁小可爱
 * @create 2020-01-10 14:29
 */
public class Ticket implements Runnable {
    // 总共一百张票
    private int ticket = 10;

    /*
     * 执行卖票操作
     * */
    @Override
    public void run() {
        // 窗口永远卖票
        while (true) {
            sellTicket();
        }
    }

    // 把存在线程安全问题的代码放入同步方法中
    public synchronized void sellTicket() {
        if (ticket > 0) {
            // 获取当前线程的名字
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在卖: " + ticket--);
        }
    }

    public static void main(String[] args) {

        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket, "窗口一");
        Thread t2 = new Thread(ticket, "窗口二");
        Thread t3 = new Thread(ticket, "窗口三");

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

 3、Lock锁 

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

package xu.com.threads.chap1;

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

public class TicketLock implements Runnable {

    // 总共一百张票
    private int ticket = 10;
    // 创建锁对象
    Lock lock = new ReentrantLock();

    /*
     * 执行卖票操作
     * */
    @Override
    public void run() {
        // 窗口永远卖票
        while (true) {
            // 同步代码块 加锁
            lock.lock();
            if (ticket > 0) {
                // 获取当前线程的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖: " + ticket--);
            }
            // 释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {

        TicketLock ticket = new TicketLock();
        Thread t1 = new Thread(ticket, "窗口一");
        Thread t2 = new Thread(ticket, "窗口二");
        Thread t3 = new Thread(ticket, "窗口三");

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

}

五、ReentrantReadWriteLock 

1、场景需求
  • 对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)
  • 在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发)
  • 但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)
  • 在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁

读写锁机制:

读锁写锁
读锁共享互斥
写锁互斥互斥

2、 代码实例

验证读读共享模式

    @Test
    public void readReadMode() throws InterruptedException {
        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock r = rw.readLock();
        ReentrantReadWriteLock.WriteLock w = rw.writeLock();
 
        Thread thread0 = new Thread(() -> {
            r.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 1 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }
        },"t1");
 
        Thread thread1 = new Thread(() -> {
            r.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 2 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }
        },"t2");
 
        thread0.start();
        thread1.start();
 
        thread0.join();
        thread1.join();
    }

验证读写互斥模式 

    @Test
    public void readWriteMode() throws InterruptedException {
        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock r = rw.readLock();
        ReentrantReadWriteLock.WriteLock w = rw.writeLock();
 
        Thread thread0 = new Thread(() -> {
            r.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 1 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }
        },"t1");
 
        Thread thread1 = new Thread(() -> {
            w.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 2 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                w.unlock();
            }
        },"t2");
 
        thread0.start();
        thread1.start();
 
        thread0.join();
        thread1.join();
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

徐泗空

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值