• 进程与线程(三)(线程安全)


    线程安全

     

    定义:如果有多个线程在同时运行,而这些线程可能会同时运行一段代码。程序每次运行结果和单线程结果是一样的,而且其他变量的值也和预期的是一样的,就是线程安全。

    线程安全案例

    这里通过一个案例来更深一步了解线程的安全问题。

    业务:电影院3个窗口卖总共100张票。也就是多线程并发访问同一个数据资源。

    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        Tickets t = new Tickets();
        //创建三个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t0.start();
        t1.start();
        t2.start();
    }
    
    public class Tickets implements Runnable {
        private int ticket = 100;
    
        public void run(){
            while(true){
                if(ticket >0){
                    System.out.println(Thread.currentThread().getName()+"出售第"+ticket--);
                }
            }
        }
    }

    上面的代码其实是有漏洞的,首先要说线程其实有个特点 ‘从哪跌倒从哪爬起来’,比如上面得代码,如果剩了最后一张票,判断ticket>0进入之后,如果此时这个线程得cpu使用权被抢走了,线程就停在了输出语句这里,没有执行ticket--得操作呢,这时候线程2就会进来,也会进入ticket>0得方法里,那么此时线程2就将输出最后一张票,并且ticket也会被赋值为0,此时由于线程已经进入了这个if里,不需要在判断了,所以线程1也会输出,不过输出就是-1了,很明显就出现了问题了。这个程序就属于线程不安全得程序。上面的代码在测试得时候可能测试很多次都不会出现-1的情况,那是因为代码量少,并且线程熟练少。如果想要看到-1的效果,可以在进入if判断之后加上一个Thread.sleep(10);意思就是让这个线程休眠10毫秒,此时这r个n线程就会让出cpu资源,别的线程就会趁机进来了。

    效果:

    同步锁

    那么对于上面的情况,如何处理呢?

    处理思路:我们希望if里面的代码在一个线程执行的时候,其他线程不能够进入,那么这样就可以保证了线程的安全了。

    处理办法:java提供的同步技术,同步代码块,锁住。

    公式:synchronized(任意对象){线程要操作的共享数据}

    同步代码块

    对上面的代码修改如下

    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        Tickets t = new Tickets();
        //创建三个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t0.start();
        t1.start();
        t2.start();
    }
    
    public class Tickets implements Runnable {
        private int ticket = 100;
        private Object object = new Object();//随便定义一个对象
    
        public void run(){
            while(true){
                //线程共享数据,保证安全,加入同步代码块
                synchronized (object) {
                    if (ticket > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "出售第" + ticket--);
                    }
                }
            }
        }
    }

    同步代码块的执行原理

    线程遇到同步代码块后,回去判断同步锁是否存在,如果不存在则线程被格挡在外,如果存在,线程获取锁,进入里面的方法并执行,执行完毕之后,离开同步代码块,线程将锁对象还回去(释放锁),优点就是保证了程序的安全性,缺点就是牺牲了程序的运行速度,但是这个是无法避免的。 

    同步方法

    区别于同步代码块的是,synchronized关键字要写在方法上。

    public class Tickets implements Runnable {
        private int ticket = 100;
        private static int ticket2 = 100;
        //使用同步方法的方式,不需要这个对象了。那么同步方法还有锁吗?有!
        // 同步方法中的对象锁,是本类对象引用this,如果方法是静态的static,锁是本类自己+.class
        //private Object object = new Object();
    
        public void run(){
            while(true){
                payTicket();
            }
        }
    
        //在方法上写上synchronized关键字,也可以达到和同步代码块一样的效果。
        public synchronized void payTicket(){
            if (ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "出售第" + ticket--);
            }
        }
    
        public static synchronized void payTicket2(){
            synchronized (Tickets.class){}//静态方法中的锁是类名+.class
            if (ticket2 > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "出售第" + ticket2--);
            }
        }
    }

    Lock(jdk1.5后新特性)

    Lock实现提供了比使用synchronized方法和语句可获得更广泛得锁定操作。

    synchronized方法或语句得使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中,当获取了多个锁时,他们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

    那么我们再对上面的代码进行修改---使用接口Lock,替换同步代码块,实现线程的安全性---lock()获取锁,unlock()释放锁,成员位置创建实现类ReentrantLock

    修改如下:

    public class Tickets implements Runnable {
        private int ticket = 100;
        //在类的成员位置,创建Lock接口的实现类对象
        private Lock lock = new ReentrantLock();
    
        public void run() {
            while (true) {
                //获取锁
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                        System.out.println(Thread.currentThread().getName() + "出售第" + ticket--);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        //释放锁--释放锁的操作最好放在finally中
                        lock.unlock();
                    }
                }
            }
        }
    }

    死锁

     

    定义

    就是多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。

    死锁出现的前提

    必须是多线程的出现同步嵌套。

    同步锁

     

    当多个线程同时访问一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行多个线程,在同一时间内只允许一个线程访问共享数据。java中可以使用synchronized关键字来取得一个对象的同步锁。

    线程等待与唤醒

     

    在了解线程等待与唤醒机制之前,需要先了解一个概念--线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。这种手段就是等待唤醒机制。

    等待唤醒机制所涉及到的方法:

    wait():等待,将正在执行的线程释放其执行资格和执行权,并存储到线程池中。

    notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。

    notifyAll():唤醒全部,可以将线程池中所有wait()线程都唤醒。

    其实,所谓唤醒的意思就是让线程池中的线程具备执行资格。必须注意的是,这些方法都是在同步中才有效。同时这些方法在使用时必须注明所属锁,这样才可以明确出这些方法操作的到底时哪个锁上的线程。

    /**
     * 同时有两个线程,对资源中的变量进行操作
     * 线程1:对name和age赋值
     * 线程2:对name和age值进行打印
     */
    public class Resource {
        public String name;
        public String sex;
        public boolean flag = false;
    }
    
    /**
     * 赋值的时候需要一次是张三一次是李四
     */
    public class Input implements Runnable {
        private Resource r;
        public Input(Resource r){//使用构造方法 保证输入和输出的resouce对象是一个
            this.r = r;
        }
        public void run(){
            int i=0;
            while(true){
                synchronized (r) {//用对象资源锁 而不是this锁 是为了保证输入和输出的锁是一个 以防出现 张三-女的情况
                    //对标记判断,如果是true,等待
                    if(r.flag){
                        try {r.wait();} catch (InterruptedException e) {e.printStackTrace(); }//无论是wait或者notify都需要用锁对象调用,否则会报错
                    }
                    if (i % 2 == 0) {
                        r.name = "张三";
                        r.sex = "男";
                    } else {
                        r.name = "李四";
                        r.sex = "女";
                    }
                    //将对方线程唤醒,标记修改为true.
                    r.flag = true;
                    r.notify();//无论是wait或者notify都需要用锁对象调用,否则会报错
                }
                i++;
            }
        }
    }
    
    public class Output implements Runnable {
        private Resource r;
        public Output(Resource r){//使用构造方法 保证输入和输出的resouce对象是一个
            this.r = r;
        }
        public void run(){
            while (true){
                synchronized (r) {//用对象资源锁 而不是this锁 是为了保证输入和输出的锁是一个 以防出现 张三-女的情况
                    //判断标记,false 等待
                    if(!r.flag){
                        try {r.wait(); } catch (InterruptedException e) { e.printStackTrace(); }//无论是wait或者notify都需要用锁对象调用,否则会报错
                    }
                    System.out.println(r.name + "....." + r.sex);
                    //标记改成false,唤醒对方线程
                    r.flag = false;
                    r.notify();//无论是wait或者notify都需要用锁对象调用,否则会报错
                }
            }
        }
    }
    
    //开启输入线程和输出线程,实现赋值和打印值
    public static void main(String[] args) {
        ////这里创建对象 是为了保证输入和输出的resouce对象是一个
        Resource r = new Resource();
        Input in = new Input(r);
        Output out = new Output(r);
        //创建三个Thread类对象,传递Runnable接口实现类
        Thread tin = new Thread(in);
        Thread tout = new Thread(out);
        tin.start();
        tout.start();
    }

    效果:(交错输出,使用flag标记)

    参考:

    1. 黑马程序员视频:多线程部分

    持续更新!!!

  • 相关阅读:
    79.Word Search
    78.Subsets
    77.Combinations
    75.Sort Colors
    74.Search a 2D Matrix
    73.Set Matrix Zeroes
    71.Simplify Path
    64.Minimum Path Sum
    63.Unique Paths II
    Docker 拉取 oracle 11g镜像配置
  • 原文地址:https://www.cnblogs.com/flyinghome/p/12516168.html
Copyright © 2020-2023  润新知