线程安全问题

多个线程并发执行时其实是我们的CPU在多个线程之间高速切换执行,以至于让我们认为是在同时执行。Java使用的是抢占式的调度模式,即哪个线程抢到了CPU资源就会执行,并不会等待其他线程执行完毕,这就会引发线程安全问题,当多线程共同操作一份数据时,当t1执行到一半时被t2抢去CPU执行权并且将变量修改成t1不想要的数据。

使用经典的卖票案例演示线程安全问题

public class MyTest {

    public static void main(String[] args) {

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

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

    }

    static class MyTicket implements Runnable{

        //        总票数 也就是共享变量
        private int ticket = 100;

        //    用来记录窗口(线程)卖出的总票数
        private Vector<Integer> vector = new Vector<>();

        @Override
        public void run() {
            while (true){
                if (ticket > 0){
//                为了体现安全问题,休眠1秒
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket--+"张票");
                    vector.add(ticket);
                }else {
                    System.out.println(Thread.currentThread().getName()+vector.size());
                    return;
                }
            }
        }
    }
}
窗口一卖出了第100张票
窗口三卖出了第99张票
窗口二卖出了第100张票
窗口一卖出了第98张票
窗口二卖出了第96张票
窗口三卖出了第97张票
窗口三卖出了第95张票
窗口二卖出了第94张票
窗口一卖出了第94张票
.......
窗口一卖出了第0张票
窗口一124
窗口三卖出了第-1张票
窗口三125
窗口二卖出了第1张票
窗口二126

从这里可以看出的确有线程问题,会出现卖同一张票的还有卖出0和负票数的,可以使用我们线程同步解决这些问题。

线程同步

Java中使用synchronized关键字解决多个线程操作同一份数据时引发的安全问题,也就是同步代码块,同步代码块必须要有一个锁对象,并且如果想要多个线程之间共享数据不会出现安全问题时,要保证锁对象一致。大概原理就是t1拿到锁便开始执行,t2此时进入阻塞状态等待t1释放锁,等t1执行完毕后自动释放锁,此时t2便开始执行,循环...

synchronized又分为同步代码块和同步方法

同步代码块

synchronized (锁对象){
    被同步的代码
}

同步方法

此时的锁对象为this

public synchronized void fun(){
    //   线程任务
}

此时的锁对象为类的class对象,也就是类名.class

public static synchronized void fun(){
    //   线程任务
}

卖票案例可改为

//        如果加在run方法上,某条线程拿到执行权直到执行完毕才会释放锁,其他线程也就没有意义了,当然也可以将同步代码块中的代码放到一个方法中,将方法设置为同步方法,在run方法中进行调用这个方法       
       @Override
        public void run() {
            while (true) {
//        这里的锁对象使用的this当前对象,因为我们实现的runnable接口,只创建了一个对象开启的几个线程,也可在成员变量位置创建对象作为锁                
                synchronized (this) {
//                    在这里会出现问题,也就是会出现卖0,负数票的
//                    比如窗口三抢到了CPU判断有1张票,准备去卖票,被窗口一抢去了,此时判断还有一张准备去卖时又被窗口二抢去
//                    此时窗口二顺利的卖出了最后一张票,窗口一再抢到后直接卖票便出现了第0张,窗口三则出现了卖负数票的情况
//                    所以选择在此处加同步代码块,保证任意线程在判断成功后可以顺利的卖出一张票
                    if (ticket > 0) {
//                       为了体现安全问题,休眠1秒
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket-- + "张票");
                        vector.add(ticket);
                    } else {
                        System.out.println(Thread.currentThread().getName() + vector.size());
                        return;
                    }
                }
            }
        }

死锁

线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,也就是锁嵌套,而synchronized特点是一个线程获得锁,在该线程未释放锁的情况下,其他线程获取不到这个锁会一直等待下去,造成无限的等待因此便造成了死锁

线程1拿着线程2需要的锁,线程2拿着线程1需要的锁

public class DieLock {

    public static void main(String[] args) {

        Object lock1 = new Object();
        Object lock2 = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock1){
                    System.out.println(Thread.currentThread().getName()+"获取到锁1");
//                    放弃cpu资源
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock2){
                        System.out.println(Thread.currentThread().getName()+"获取到锁2");
                    }
                }
            }
        },"线程1").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock2){
                    System.out.println(Thread.currentThread().getName()+"获取到锁2");
//                    放弃cpu资源
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock1){
                        System.out.println(Thread.currentThread().getName()+"获取到锁1");
                    }
                }
            }
        },"线程2").start();
    }
}

// console
线程1获取到锁1
线程2获取到锁2
等待(产生死锁)

等待唤醒机制

多个线程在处理同一个资源,但是处理的动作(线层的任务)却不相同。通过一定的手段使各个线程能有效的利用资源(线程之间的通信)。而这种手段即等待唤醒机制。

涉及方法:

wait():令当前线程挂起并放弃CPU,同步资源下使别的线程可访问并修改共享数据,当前线程可在被唤醒后排队等待再次对资源的访问

notify():唤醒,随机唤醒一个正在排队等待同步资源的线程

notifyAll():唤醒全部,唤醒正在排队等待资源的所有线程,结束等待

唤醒也就是让线程获得执行资格,这些方法必须在同步中才有效,同时在使用时需要标明锁对象,都属于Object类中的方法,这些方法在使用时都需要标明锁,而锁又可以是任意对象,能被任意对象调用的方法在Object类中。

交替打印案例

public class AfterNateTest {

    //    设置一个标记 用于确定线程是否需要等待
    private static boolean flag = false;
    //    锁对象
    private static Object lock = new Object();

    public static void main(String[] args) {

//        两个原数组,设置长度不一致
        String[] chars = {"a", "b", "c", "d", "e", "f", "g"};
        Integer[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};

//        开启两个线程传入不同的数组
        new Thread(new Runnable() {
            @Override
            public void run() {
                print(chars, lock);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                print(arr, lock);
            }
        }).start();

    }

    public static void print(Object[] array, Object lock) {
        for (int i = 0; i < array.length; i++) {
//            保持同步
            synchronized (lock) {
//                wait()等待需要手动唤醒,在线程开始前唤醒其他在等待的线程,让其准备就绪
                lock.notify();
//               打印当前线程的名字和字符
                System.out.println(Thread.currentThread().getName() + "===" + array[i]);
//                判断是否为最后一个元素
                if (i == array.length - 1) {
                    System.out.println(Arrays.toString(array));
//                    将标记改为true
                    flag = true;
//                    停止当前线程(循环结束的)
                    Thread.currentThread().stop();
                } else {
                    try {
//                        判断如果不是true说明还有元素,需要交替打印,在执行完输出后进入等待状态
                        if (!flag) {
                            lock.wait();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

效果

Thread-0===a
Thread-1===1
Thread-0===b
Thread-1===2
Thread-0===c
Thread-1===3
Thread-0===d
Thread-1===4
Thread-0===e
Thread-1===5
Thread-0===f
Thread-1===6
Thread-0===g
[a, b, c, d, e, f, g]
Thread-1===7
Thread-1===8
Thread-1===9
[1, 2, 3, 4, 5, 6, 7, 8, 9]

调用wait()会立即进入等待状态并释放锁资源,notify()/notifyAll()可以唤醒其他等待的线程,但是需要执行完所在的synchronized代码块中的代码

public class MyTest2 {

    public static void main(String[] args) {

        Object lock = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread1 start");
                synchronized (lock){
                    System.out.println("Thread1 wating");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread1 continue");
                    System.out.println("Thread1 over");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread2 start");
                synchronized (lock){
                    lock.notify();
                    System.out.println("Thread2 running");
                    try {
                        System.out.println("Thread2 sleep");
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread2 over");
                }
            }
        }).start();
    }
}
// console
Thread1 start
Thread1 wating
Thread2 start
Thread2 running
Thread2 sleep
Thread2 over
Thread1 continue
Thread1 over

Lock锁

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

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

// 加同步锁
public void lock()
// 释放同步锁
public void unlock()
public class MyTest3 implements Runnable{

    private int ticket = 100;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
//        卖票窗口一直开启
        while (true){
//            上锁
            lock.lock();
            if (ticket > 0){
                try {
//                    模拟出票时间
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"=="+ticket--);
            }
//            释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyTest3 myTest3 = new MyTest3();
        new Thread(myTest3).start();
        new Thread(myTest3).start();
    }
}

总结

同步锁

多个线程想保证线程安全,必须要使用同一个锁对象(可以是任意对象)

非静态同步方法的锁对象是this,静态同步方法的锁对象是类.class

sleep()和wait()方法的区别

sleep:不释放锁(即使是在同步区域内),只释放cpu资源,在休眠时间内,不能唤醒,可以写在任意位置

wait:释放锁对象并释放cpu执行权,在等待的时间内,能唤醒,只能存在与同步中

为什么wait(),notify()和notifyAll()都定义在Object类中

这样锁对象便可以是任意对象(类都默认继承自Object)

线程状态图

img

Copyright © TaoQZ 2019 all right reserved,powered by Gitbook作者联系方式:taoqingzhou@gmail.com 修订时间: 2024-11-19 17:25:43

results matching ""

    No results matching ""

    results matching ""

      No results matching ""