moc 发表于 2019-1-13 21:09:56

Java031-线程安全

本帖最后由 moc 于 2019-1-13 21:09 编辑

1、简介
模拟电影院多个窗口售票案例,启动三个线程,一起对公共资源进行操作。
线程实现方式1:
// 线程类
public class SellTicket extends Thread {
        private static int tickets = 100; //为了让多个线程对象共享,用静态修饰

        @Override
        public void run() {
                while (true) {
                        if (tickets > 0) {
                                try {
                                        Thread.sleep(100); // 稍作休息
                                } catch (InterruptedException e) {
                                        e.printStackTrace();
                                }
                                System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
                        }
                        else {
                                break;
                        }
                }
        }
}
// 测试类
public class SellTicketDemo {
        public static void main(String[] args) {
                // 创建三个线程对象
                SellTicket st1 = new SellTicket();
                SellTicket st2 = new SellTicket();
                SellTicket st3 = new SellTicket();

                // 给线程对象起名字
                st1.setName("窗口1");
                st2.setName("窗口2");
                st3.setName("窗口3");

                // 启动线程
                st1.start();
                st2.start();
                st3.start();
        }
}线程实现方式2:
// 线程类
public class SellTicket implements Runnable {
        private int tickets = 100;

        @Override
        public void run() {
                while (true) {
                        if (tickets > 0) {
                                try {
                                        Thread.sleep(100); // 稍作休息
                                } catch (InterruptedException e) {
                                        e.printStackTrace();
                                }
                                System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                        }
                        else {
                                break;
                        }
                }
        }
}
// 测试类
public class SellTicketDemo {
        public static void main(String[] args) {
                // 创建资源对象
                SellTicket st = new SellTicket();

                // 创建三个线程对象
                Thread t1 = new Thread(st, "窗口1");
                Thread t2 = new Thread(st, "窗口2");
                Thread t3 = new Thread(st, "窗口3");

                // 启动线程
                t1.start();
                t2.start();
                t3.start();
        }
}出现的问题:
        ① 相同的票卖了多次==>CPU的一次操作必须是原子性的
        ② 出现了负数票 ==> 线程的随机性及延迟导致
这就是多线程操作导致的共享数据操作出现问题,即线程不安全。
导致线程不安全的原因:
        ① 多线程环境
        ② 存在共享数据
        ③ 存在多条语句操作共享数据(读、写)
2、线程同步
1. 同步代码块
        同步的关键是管程(也叫信号量semaphore)的概念。管程是一个互斥独占锁定的对象,或称互斥体(mutex)。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定,它必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其他的线程被称为等待管程。一个拥有管程的线程如果愿意的话可以再次进入相同的管程。
格式:
        synchronized(对象){
                需要同步的代码;
        }
注意:
        ① 同步体的内容相当于一个原子操作;
        ② 多个线程必须是同一把锁;
        ③ 锁对象可以是任意对象。
// 线程类
public class SellTicket implements Runnable {
        private int tickets = 100;// 定义100张票
        private Object obj = new Object();//创建锁对象
       
        @Override
        public void run() {
                while (true) {
                        synchronized (obj) {
                                if (tickets > 0) {
                                        try {
                                                Thread.sleep(100);
                                        } catch (InterruptedException e) {
                                                e.printStackTrace();
                                        }
                                        System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                                }
                                else {
                                        break;
                                }
                        }
                }
        }
}
// 测试类
public class SellTicketDemo {
        public static void main(String[] args) {
                SellTicket st = new SellTicket();                // 创建资源对象

                Thread t1 = new Thread(st, "窗口1");
                Thread t2 = new Thread(st, "窗口2");
                Thread t3 = new Thread(st, "窗口3");

                t1.start();
                t2.start();
                t3.start();
        }
}2. [静态]同步方法
即在该方法有关键字synchronized。
同步方法的锁对象是this,即本类。
静态同步方法的锁对象是Class对象,即类的字节码文件对象。
private synchronized void sellTicket() {
        if (tickets > 0) {
        try {
                        Thread.sleep(100);
        } catch (InterruptedException e) {
                        e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()        + "正在出售第" + (tickets--) + "张票 ");
        }
}3、锁 --- Lock
包: java.util.concurrent.locks
方法:
        void lock()-------------->获取锁
        void unlock()-----------> 释放锁
发生异常时不会自动释放锁,因此一般使用Lock必须在try{}catch{}块中进行:
try{
    //处理任务
}catch(Exception ex){
    //处理异常   
}finally{
    lock.unlock();   //释放锁
}可重入锁 --- ReentrantLock:
ReentrantLock是实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
public class SellTicket implements Runnable {
        private int tickets = 100;
        private Lock lock = new ReentrantLock();// 定义锁对象

        @Override
        public void run() {
                while (true) {
                        try {
                                // 加锁
                                lock.lock();
                                if (tickets > 0) {
                                        try {
                                                Thread.sleep(100);
                                        } catch (InterruptedException e) {
                                                e.printStackTrace();
                                        }
                                        System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                                }
                                else {
                                        break;
                                }
                        } finally {
                                // 释放锁
                                lock.unlock();
                        }
                }
        }
}4、死锁
死锁概念:   
        当一个线程永远地持有一个锁,并且其他线程都尝试去获得这个锁时,那么它们将永远被阻塞。
        如果线程A持有锁L并且想获得锁M,线程B持有锁M并且想获得锁L,那么这两个线程将永远等待下去,这种情况就是最简单的死锁形式。
        一个类可能发生死锁,并不意味着每次都会发生死锁,这只是表示有可能。当死锁出现时,往往是在最糟糕的情况----高负载的情况下。
// 创建两把锁对象
public class MyLock {
        public static final Object objA = new Object();
        public static final Object objB = new Object();
}
// 容易发生死锁类
public class DieLock extends Thread {
        private boolean flag;
        public DieLock(boolean flag) {
                this.flag = flag;
        }

        @Override
        public void run() {
                if (flag) {
                        synchronized (MyLock.objA) {
                                System.out.println("if objA");
                                synchronized (MyLock.objB) {
                                        System.out.println("if objB");
                                }
                        }
                } else {
                        synchronized (MyLock.objB) {
                                System.out.println("else objB");
                                synchronized (MyLock.objA) {
                                        System.out.println("else objA");
                                }
                        }
                }
        }
}
// 测试类
public class DieLockDemo {
        public static void main(String[] args) {
                DieLock dl1 = new DieLock(true);
                DieLock dl2 = new DieLock(false);

                dl1.start();
                dl2.start();
        }
}避免死锁的方式:
        ① 让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实
        ② 设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量
        ③ 既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息。
5、线程间通信---> 生产者和消费者模式
Object类中提供了几个方法:
public final void wait() --------------> 线程进入等待阻塞状态,让出CPU执行权。
public final void notify() ------------> 唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中任意一个线程。
public final void notifyAll() ---------> 唤醒在此对象监视器上等待的所有线程。
生产者和消费者模型是线程模型中的经典模型:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。
--> 资源类:Student       
--> 设置学生数据:SetThread(生产者)
--> 获取学生数据:GetThread(消费者)
--> 测试类:StudentDemo
// 资源类
public class Student {
        String name;
        int age;
        boolean flag;// 默认情况是没有数据,如果是true,说明有数据
}
// 生产者类
public class SetThread implements Runnable {
        private Student s;
        private int x = 0;
        public SetThread(Student s) {
                this.s = s;
        }

        @Override
        public void run() {
                while (true) {
                        synchronized (s) {
                                //判断有没有
                                if(s.flag){//有,需要等待资源被消耗
                                        try {
                                                s.wait();   //进入等待,释放锁
                                        } catch (InterruptedException e) {
                                                e.printStackTrace();
                                        }
                                }
                                // 没有或已经被消费,开始生产
                                if (x % 2 == 0) {
                                        s.name = "牛顿";
                                        s.age = 70;
                                } else {
                                        s.name = "莱布尼茨";
                                        s.age = 67;
                                }
                                x++;
                               
                                //修改标记
                                s.flag = true;
                                //生产完毕,唤醒消费线程
                                s.notify(); //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
                        }
                }
        }
}
// 消费者类
public class GetThread implements Runnable {
        private Student s;
        public GetThread(Student s) {
                this.s = s;
        }

        @Override
        public void run() {
                while (true) {
                        synchronized (s) {
                                //判断有没有
                                if(!s.flag){
                                        try {
                                                s.wait(); //进入等待,立即释放锁。将来醒过来的时候,仍从这里醒过来的时候
                                        } catch (InterruptedException e) {
                                                e.printStackTrace();
                                        }
                                }
                               
                                System.out.println(s.name + "---" + s.age);                               
                                //修改标记
                                s.flag = false;
                                //消费结束,唤醒生产线程
                                s.notify();
                        }
                }
        }
}
// 测试类
public class StudentDemo {
        public static void main(String[] args) {
                //创建资源
                Student s = new Student();
               
                //设置和获取的类
                SetThread st = new SetThread(s);
                GetThread gt = new GetThread(s);

                //线程类
                Thread t1 = new Thread(st);
                Thread t2 = new Thread(gt);

                //启动线程
                t1.start();
                t2.start();
        }
}
页: [1]
查看完整版本: Java031-线程安全