ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2020-03-22 09:00:43  阅读:231  来源: 互联网

标签:同步 Thread 安全 线程 进程 new ticket public


线程安全

 

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

 

线程安全案例

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

业务:电影院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. 黑马程序员视频:多线程部分

持续更新!!!

 

标签:同步,Thread,安全,线程,进程,new,ticket,public
来源: https://www.cnblogs.com/flyinghome/p/12516168.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有