顺着我的思路,一步一步往下看,你会有所收获。。。。
实现多线程有两种方式,代码如下
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。