• [一]多线程编程-实现及锁机制


    顺着我的思路,一步一步往下看,你会有所收获。。。。

    实现多线程有两种方式,代码如下

    1.继承Thread类:

    code1:

    public class Test {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
            ticket.start();
        }
    }
    class Ticket extends Thread{
        @Override
        public void run() {
            System.out.println("Hello ....");
        }
    }
    
    执行结果:Hello ....

    2.实现Runnable接口

    code2:

    public class Test {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
            new Thread(ticket).start();
        }
    }
    class Ticket implements Runnable{
        @Override
        public void run() {
            System.out.println("Hello ....");
        }
    }
    
    执行结果:Hello ....

    在Java API 中,我们可以找到很多Thread封装的方法,当我们创建的线程数比较多的时候,我们可以为每个线程创建名称

    code3:

    class Ticket implements Runnable{
        @Override
        public void run() {
            System.out.println("Hello ...."+Thread.currentThread().getName());
        }
    }
    
    执行结果:Hello ....Thread-0
    是不是觉得这个名字不好看?
    线程默认名称都是:Thread-0、Thread-1 。。n

    查找API,我们得知Thread类中有一个super(String name)方法,这个方法是给线程命名的,也就是说,我们继承了Thread类的子类,能够将线程名称替换掉

    code4:

    public class Test {
        public static void main(String[] args) {
            Ticket ticket = new Ticket("Ticket");
            ticket.start();
        }
    }
    class Ticket extends Thread{
        Ticket(String name){
            super(name);
        }
        @Override
        public void run() {
            System.out.println("Hello ...."+Thread.currentThread().getName());
        }
    }
    执行结果:Hello ....Ticket

    阅读到此处,相信你已经了解了创建线程的方法,接下来,我们看一个简单的售票例子,假设同时有两个售票窗口售票,一共有5张票可以卖:code:5

    public class Test {
        public static void main(String[] args) {
            Ticket one = new Ticket("一号");
            Ticket two = new Ticket("二号");
            one.start();
            two.start();
        }
    }
    class Ticket extends Thread{
        private int ticket = 5;
        Ticket(String name){
            super(name);
        }
        @Override
        public void run() {
            while(true){
                if(ticket>0)
                    System.out.println(Thread.currentThread().getName()+"窗口卖票..."+ ticket--);
            }
        }
    } 
    执行结果:
      一号窗口卖票...5
      一号窗口卖票...4
      一号窗口卖票...3
      一号窗口卖票...2
      一号窗口卖票...1
      二号窗口卖票...5
      二号窗口卖票...4
      二号窗口卖票...3
      二号窗口卖票...2
      二号窗口卖票...1
    

    共卖出了10张票,什么原因导致的?我们来分析下:

    通过继承Thread类,定义了ticket=5(票数),然后在main方法中创建了两个Ticket售票窗口线程,再调用start方法来开启线程,问题就在,线程中的票数ticket没有被共享,它是属于每个单独的线程的,

    一号有5张票,二号有5张票,So....  问题找到了,既然继承Thread类搞定不了,那么我们来试试实现Runnable方法

    code6:

    public class Test {
        public static void main(String[] args) {
            Ticket one = new Ticket();  
            new Thread(one).start(); 
            new Thread(one).start();
        }
    }
    class Ticket implements Runnable{
        private int ticket = 5;
        @Override
        public void run() {
            while(true){
                if(ticket>0)
                    System.out.println(Thread.currentThread().getName()+"窗口卖票..."+ ticket--);
            }
        }
    }
    执行结果:
      Thread-0窗口卖票...5
      Thread-0窗口卖票...3
      Thread-0窗口卖票...2
      Thread-0窗口卖票...1
      Thread-1窗口卖票...4
    

    每次执行,顺序可能都不一致,但结果是正确的,卖出了5张票。

    你可能会想,为什么不创建两个Ticket对象,再创建两个线程分别来start()呢,如下代码

    code7:

    public static void main(String[] args) {
      Ticket one = new Ticket();
      Ticket two = new Ticket();
      new Thread(one).start();
      new Thread(two).start();
    }
    class Ticket {
      内容不变...
    }
    执行结果:
      Thread-0窗口卖票...5
      Thread-1窗口卖票...5
      Thread-0窗口卖票...4
      Thread-1窗口卖票...4
      Thread-0窗口卖票...3
      Thread-1窗口卖票...3
      Thread-0窗口卖票...2
      Thread-1窗口卖票...2
      Thread-0窗口卖票...1
      Thread-1窗口卖票...1
    

    看执行结果,卖出了双份票,成员变量ticket还是没有被共享。。。懂了吧。。。。

    回过头来看代码code:6,这一步执行结果正确,难道就真的没问题了吗?看下面代码

    code8:

    class Ticket implements Runnable{
        private int ticket = 1000;
        @Override
        public void run() {
            while(true){
                if(ticket>0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"窗口卖票..."+ ticket--);
                }
            }
        }
    }

    分析:在判断ticket条件中,加了一个Thread.sleep(10)方法,让当前线程进来的是时候睡个10毫秒,你会发现结果与预期的不一致

    执行结果:
      ....
      Thread-1窗口卖票...4
      Thread-0窗口卖票...3
      Thread-1窗口卖票...2
      Thread-1窗口卖票...1
      Thread-0窗口卖票...0
    

    我们卖出了0号票,多执行几次,可能还会卖出-1、-2号票

    这里涉及一个知识点:线程安全,那我们接下来就学习下,什么是线程安全,百度百科如下:

    定义:

    个人总结:多线程访问同一代码,不会产生不确定的结果

    如何做到线程安全?两个字:同步(synchronized),百度到同步的方式有多种,同步代码块、同步函数(方法)

    1.同步代码块:

    语法:synchronized (锁对象){
          需要被同步的代码
       }

     同步前提:

       1.必须要有两个或以上的线程

       2.必须是多个线程使用同一个锁

      怎么判断哪些代码需要同步:

      1.哪些代码是多线程运行代码

      2.哪些数据是共享数据

      3.哪些多线程代码是操作共享数据的

    下面的ticket就是共享数据(A窗口卖过了的票,B窗口就不能再卖了)

    code9:

    class Ticket implements Runnable{
        private int ticket = 100;
        Object obj = new Object();
        @Override
        public void run() {
            while(true){
                synchronized (obj){
                    if(ticket>0){
                        try {
                            Thread.sleep(10)
                  System.out.println(Thread.currentThread().getName()+"窗口卖票..."+ ticket--);
                } catch (InterruptedException e) { 
                   e.printStackTrace(); 
                }
              }
            }
          }
       }
     }
    执行结果:
      .....
      Thread-0窗口卖票...6
      Thread-0窗口卖票...5
      Thread-1窗口卖票...4
      Thread-1窗口卖票...3
      Thread-1窗口卖票...2
      Thread-1窗口卖票...1
    

    暂时先不讲为什么要放一个obj(你可以放别的,例如this,下文中会介绍这个锁对象的),加了同步后结果正确了。为什么加了同步代码块,就Ok了呢 ?

    分析:现在有两个线程(上面说的两个买票窗口),分别叫A跟B,假设A调用run方法时进入同步代码快,获得了当前代码的执行权并锁定,此时如果B进来,B是执行不了同步代码块中的内容的,B要等待A执行完成,才能进入同步代码块内锁定代码并执行相应内容

    案例:大家都坐过火车吧,你进厕所,把门锁了,就你能上,别人要在门口等着你,你上完了(代码执行完了),把门打开了(释放锁),别人才能进去,当然也有可能你刚打开门,然后你又拉肚子了,然后又进去了。。。哈哈。。

    好处:解决了多线程的安全问题

    弊端: 多个线程需要判断锁,比较消耗资源

    2.同步函数(方法),既然同步代码块是用来封装代码的,函数也有同样的功能,那么我们来试试

    code10:

    class Ticket implements Runnable{
        private int ticket = 100;
        @Override
        public void run() {
            while(true){
                this.sale();
            }
        }
        public synchronized void sale(){
            if(ticket>0){
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName()+"窗口卖票..."+ ticket--);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    执行结果与code9 一致,正确。 

    区别于code9中的同步代码块中的obj锁对象,那么同步函数的锁对象是谁呢?

    猜想:code10中用的this.sale()调用售票方法,this代表当前对象Ticket,那么同步函数的锁,就是当前对象Ticket,看下面代码,证明这个猜想

    code11:

    public class Test {
        public static void main(String[] args) {
            try {
                Ticket one = new Ticket();
                new Thread(one).start();
                Thread.sleep(10);
                one.flag = false;
                new Thread(one).start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    class Ticket implements Runnable{
        private int ticket = 1000;
        private Object obj = new Object();
        boolean flag = true;
        @Override
        public void run() {
            if(flag){
                synchronized(obj){
                    while(true){
                        if(ticket>0){
                            System.out.println(Thread.currentThread().getName()+"同步代码块..."+ ticket--);
                        }
                    }
                }
            }else{
                while(true)
                    this.sale();
            }
        }
        public synchronized void sale(){ //this
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"同步方法..."+ ticket--);
            }
        }
    }
    执行结果(可能与你的执行结果不一致):
      .....
      Thread-1同步代码块...3
      Thread-0同步代码块...2
      Thread-0同步代码块...1
      Thread-0同步代码块...0 
    

    代码分析:  main方法执行,创建两个线程,第一个线程调用start()获得执行权,主线程main继续往下执行,睡10毫秒,将变量设置为false,另一个线程调用start()获得执行权,主线程执行结束,现在就剩两个售票线程了(一个线程执行同步代码块的内容,另一个线程执行同步函数的内容)

    我们发现出现了0号票,也就是线程不安全了?为什么?我明明加了同步方法,也加了同步代码块,为什么还是线程不安全的呢?

    回顾上面所说的同步的两个前提:

       1.必须要有两个或以上的线程

       2.必须是多个线程使用同一个锁

    两个条件都满足了吗?看看条件1,满足了,那就是条件2出了问题了咯 ???

    code11中,同步代码块中,用的是obj对象,而同步函数中,用的是this,那么到此,我们可以肯定的是,同步函数肯定用的不是obj,对吧? 上面猜想中,我说的同步函数用的是this,那么,我们把obj改成this,如下:

    code12:

    class Ticket implements Runnable{
        private int ticket = 1000;
        //private Object obj = new Object();
        boolean flag = true;
        @Override
        public void run() {
            if(flag){
                synchronized(this){
                    while(true){
                        if(ticket>0){
                            System.out.println(Thread.currentThread().getName()+"同步代码块..."+ ticket--);
                        }
                    }
                }
            }else{
                while(true)
                    this.sale();
            }
        }
        public synchronized void sale(){ //this
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"同步方法..."+ ticket--);
            }
        }
    }
    执行结果:
      .....
      Thread-1同步代码块...3
      Thread-0同步代码块...2
      Thread-0同步代码块...1
    

    线程安全了,没有出现0号票。

    结论:同步函数用的锁是this

    此时,我们了解到,同步函数用的锁是 this ,那么我们接下来,在同步函数上加下个静态标示符static试试

    public class Test {
        public static void main(String[] args) {
            try {
                Ticket one = new Ticket();
                new Thread(one).start();
                Thread.sleep(10);
                one.flag = false;
                new Thread(one).start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    class Ticket implements Runnable{
        private static int ticket = 1000;
        boolean flag = true;
        @Override
        public void run() {
            if(flag){
                synchronized(this){
                    while(true){
                        if(ticket>0){
                            System.out.println(Thread.currentThread().getName()+"同步代码块..."+ ticket--);
                        }
                    }
                }
            }else{
                while(true)
                    this.sale();
            }
        }
        public static synchronized void sale(){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName()+"同步方法..."+ ticket--);
            }
        }
    }
    执行结果:
      ....   
      二号窗口卖票...2
      二号窗口卖票...1
      二号窗口卖票...0
    

     好吧,又出现了0号票。线程又不安全了。思考线程安全的连个前提:

      1.必须要有两个或以上的线程

      2.必须是多个线程使用同一个锁

    肯定是2没满足,那么,静态同步函数的锁对象不是this,是什么呢?

    我们知道静态资源的特点:进内存的时候,内存中没有本类的对象,那么有谁?静态方法是不是由类调用的 ?类在进内存的时候,有对象吗? 有,就是那份字节码文件对象(Ticket.class),Ticket进内存,紧跟着,静态资源进内存,OK,我们来试试。。

    将上面同步代码块中的this锁换成如下:

    synchronized(Ticket.class){
         while(true){
              if(ticket>0){
                    System.out.println(Thread.currentThread().getName()+"同步代码块..."+ ticket--);
              }
         }
    执行结果:
    Thread-0同步代码块...5
    Thread-0同步代码块...4
    Thread-0同步代码块...3
    Thread-0同步代码块...2
    Thread-0同步代码块...1
    

    最后一张为1号票,线程安全。

    结论:静态同步函数使用的锁是该方法所在类的字节码文件对象,也就是 类名.class。

  • 相关阅读:
    开源框架/软件汇总
    如何查看Maven项目的jar包依赖
    我的前端技术栈(2018版)
    解决在Mac上用pyenv安装python3失败的问题
    学习jenv
    学习sbtenv
    解决MAC下修改系统文件没权限的问题
    学习Spring Boot
    学习音标
    C# 对List中的Object进行排序
  • 原文地址:https://www.cnblogs.com/wangfajun/p/6547648.html
Copyright © 2020-2023  润新知