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

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

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

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。

原文地址:https://www.cnblogs.com/wangfajun/p/6547648.html