zoukankan      html  css  js  c++  java
  • 多线程的同步锁和死锁

    看完下面这些之后请点击以下链接,该链接中有一些练习题,可以加深理解多线程:

    https://blog.csdn.net/wenzhi20102321/article/details/52524545

    线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。什么是多线程呢?即就是一个程序中有多个线程在同时执行。

    单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。

    多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。

    程序运行原理

    分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

     抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    随机的意思是:以下面的售票为例,甲乙丙,三个售票员,用多线程同时售票,可能出现全部都是甲在售票,也可能全部都是乙在售票,也可能全部都是丙在售票,也可能有甲也有乙,或者有乙也有丙等等现象,多运行几次就会出现各种情况;

    大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

    实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。

    其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。 

     线程安全

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

    复制代码
    /**
     * 
     * @author LYJ
     *    实现Runnable的代码
     *    
     */
    public class Ticket implements Runnable {
        //设置总票数为100,这里的ticket是成员变量,
        //由于在测试类中new了一次,所以值存在一个,被三个售票窗口共享
        int ticket=100;
        public void run() {
            //模拟售票
            while(true) {
                //如果票数大于0,继续售票
                if(ticket>0) {
                    //为了让线程安全问题效果明显些,加入线程定时休眠Thread.sleep()
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    //Thread.currentThread()是线程获取当前线程对象的方法    getName()获取调用者的线程名
                    System.out.println(Thread.currentThread().getName()+"正在售票:"+ticket--);
                }
            }
    
        }
    
    /**
     * 
     * 开启多线程的代码
     *
     */
    public class ThreadDemo01 {
        public static void main(String[] args) {
            //创建Ticket的Runnable对象
            Ticket ticket = new Ticket();
            //创建线程3个对象模拟三个售票窗口,并把Runnable对象加入Thread和给Thread命名
            new Thread(ticket,"窗口1").start();;
            new Thread(ticket,"窗口2").start();;
            new Thread(ticket,"窗口3").start();;
    
        }
    *****************************************************************
    输出结果:
    窗口3正在售票:3
    窗口2正在售票:2
    窗口1正在售票:1
    窗口3正在售票:0
    窗口2正在售票:-1
                结果中出现了负数和0,这就是线程安全问题,要怎么解决呢?
                加同步锁 synchronized(Object o){....} o可以是任意对象
    ******************************************************************************
    加入同步锁后的代码
    public class Ticket implements Runnable {
        //设置总票数为100,这里的ticket是成员变量,
        //由于在测试类中new了一次,所以值存在一个,被三个售票窗口共享
        int ticket=100;
        public void run() {
            //模拟售票
            while(true) {
                //如果票数大于0,继续售票
                
                //加入同步锁
                synchronized(this) {
                if(ticket>0) {
                    //为了让线程安全问题效果明显些,加入线程定时休眠Thread.sleep()
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    //Thread.currentThread()是线程获取当前线程对象的方法    getName()获取调用者的线程名
                    System.out.println(Thread.currentThread().getName()+"正在售票:"+ticket--);
                    }
                }
            }

        }
    **********************************************
    运行几次,发现运行结果中没有出现负数和0
    复制代码

    同步方法:在方法声明上加上synchronized

    public synchronized void method(){

        可能会产生线程安全问题的代码

    }  

    同步方法中的锁对象是 this(即调用者对象)

    静态同步方法: 在方法声明上加上static synchronized

    public static synchronized void method(){

    可能会产生线程安全问题的代码

    }

    静态同步方法中的锁对象是 类名.class(因为在加载类文件的时候,静态同步方法由于是静态的也被加载进内存了,类名.class的加载优先级高于静态方法)

    同步代码块:在需要同步的代码外面包上一个synchronized

    (Object o){

        可能会产生线程安全问题的代码

    } 

    同步代码块中的锁对象可以是任意对象,也就是synchronized后边括号里面的值。

    同步锁的概念就是这样的。任何一个Object Reference都可以作为同步锁。我们可以把Object Reference理解为对象在内存分配系统中的内存地址。因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为什么我在前面的代码中声明锁对象的时候,使用了final关键字,这就是为了保证锁对象的Object Reference在整个系统运行过程中都保持不变。

     

      如果有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。
      这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程能够获得lock1的所有权,只有一个线程可以执行代码段A或者代码段B。其他竞争失败的线程只能暂停运行,进入到该同步锁的就绪(Ready)队列
      每一个同步锁下面都挂了几个线程队列,包括就绪(Ready)队列,待召(Waiting)队列等。比如,lock1对应的就绪队列就可以叫做lock1 - ready queue。每个队列里面都可能有多个暂停运行的线程。
      注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也可以翻译为等待队列)。就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。 成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者还是要乖乖地待在就绪队列中。

     

    死锁

    同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

    synchronzied(A锁){

      synchronized(B锁){       

      }

    }

    复制代码
    /**
     * 
     *创建锁对象
     *
     */
    public class Lock {
        //这里用private封装,为了不让外面随便造锁,限制只能有A,B锁个一把,这样容易出现死锁
        //即A同学和B同学想相互串门,可是没人只有一把自己房间的钥匙,而且各自都不愿意先给,于是死锁
        private Lock() {};
        public static final Object lockA =new Object();
        public static final Object lockB = new Object();
        //这里使用static 为了让外界可以通过类名调用成员变量lockA和lockB
        //因为外面无法创建Lock对象,为了让外面在不创对象的情况下调用,加了static,通过类名加变量名访问
    }
    
    /**
     * 线程任务类
     * 
     */
    import java.util.Random;
    
    public class ThreadTask implements Runnable {
        int x = new Random().nextInt(1);//用随机数随机获取0、1,来模拟CPU随机分配执行权的行为
        @Override
        public void run() {
            while(true) {
                if(x%2==0) {
                    //情况一
    //                先执行A再执行B:即A同学先拿了A门的钥匙去开A门,然后打算开B门
                    synchronized(Lock.lockA) {
                        System.out.println("A同学...开A门");
                        synchronized(Lock.lockB) {
                            System.out.println("A同学...开B门");
                        }
                    }
                }else {
                    //情况二
    //                先执行B执行A:B同学先拿了B门的钥匙,去开B门,然后打算开A门
                    synchronized(Lock.lockB) {
                        System.out.println("B同学...开B门");
                        synchronized(Lock.lockA) {
                            System.out.println("B同学...开A门");
                        }
                    }
                }
                x++;
            }
    
        }
    
    /**
     * 
     * 线程测试类
     *
     */
    public class ThreadDemo {
        public static void main(String[] args) {
            //创建Runnable的实现类对象
            ThreadTask tt = new ThreadTask();
            //把Runnable实现类对象加入线程中,创建2个线程
            Thread t1 = new Thread(tt);
            Thread t2 = new Thread(tt);
            t1.start();
            t2.start();
            
        }
    *********************************************************
    输出结果:A同学...开A门
         A同学...开B门
         B同学...开B门
         B同学...开A门
         A同学...开A门
         B同学...开B门
    结论:A同学或者B同学,一个人先后拿走两把钥匙时,线程是正常运行的,一旦A拿了A锁进去A门的时候,CPU突然让B开始执行,让B拿了B锁进入B门,结果A需要B锁,B也需要A锁,两者又不能后退
    于是死锁现象发生了。

     等待唤醒机制

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

    其他个人认为需要了解的信息:详细链接  https://zhidao.baidu.com/question/1770237924491868020.html

    Object对象的wait()方法就是等待通知,Object对象的notify()方法就是发出通知。
    具体调用方法为
    (1)等待某个信号量的通知
    public static final Object signal = new Object();

    … f1() {
    synchronized(singal) { // 首先我们要获取这个信号量。这个信号量同时也是一个同步锁

    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
    signal.wait(); // 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列

    // 可怜。辛辛苦苦

    争取到手的信号量,就这么被放弃了

    // 等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面
    // 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。
    // 仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。

    }
    }

    需要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易导致误解。signal.wait()的意思并不是说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。

    (2)发出某个信号量的通知
    … f2() {
    synchronized(singal) { // 首先,我们同样要获取这个信号量。同时也是一个同步锁。

    // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
    signal.notify(); // 这里,我们通知signal的待召队列中的某个线程。

    // 如果某个线程等到了这个通知,那个线程就会转到就绪队列中
    // 但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行
    // 嘿嘿,虽然本线程好心通知其他线程,
    // 但是,本线程可没有那么高风亮节,放弃到手的同步锁
    // 本线程继续执行下面的代码

    }
    }

    需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal这个对象本身。而是通知正在等待signal信号量的其他线程。

    以上就是Object的wait()和notify()的基本用法。
    实际上,wait()还可以定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,自己就从待召队列转移到就绪队列中了。
    另外,还有一个notifyAll()方法,表示通知待召队列里面的所有线程。
    这些细节问题,并不对大局产生影响。

    看完以上这些之后请点击以下链接,该链接中有一些练习题,可以加深理解多线程:

    https://blog.csdn.net/wenzhi20102321/article/details/52524545

  • 相关阅读:
    MS SQL SERVER导出表结构到Excel
    Ajax.ActionLink用法
    Layer弹出层关闭后刷新父页面
    Ajax.BeginForm提示不支持live属性或方法的错误
    BootStrap带样式打印
    利用JQuery jsonp实现Ajax跨域请求 .Net 的*.handler 和 WebService,返回json数据
    Bootstrap打印问题
    EF的表左连接方法Include和Join
    vs code的local history插件
    idea debug的drop frame,set watch和设置过滤条件
  • 原文地址:https://www.cnblogs.com/qcq0703/p/8665336.html
Copyright © 2011-2022 走看看