Java多线程
Posted 暴走小骚年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程相关的知识,希望对你有一定的参考价值。
本节内容:
- 什么是线程
- 多线程并行和并发的区别
- 多线程程序实现方式
- 多线程中的一些方法
- 多线程之线程同步
- 线程安全
- 多线程的死锁
- 线程安全的类
- 多线程设计模式之单例设计模式
- 线程组的概述和使用
- 线程的五种状态
- 线程池的概述和使用
- 设计模式之简单工厂模式
- 设计模式之工厂方法模式
如果对什么是线程、什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内。一篇浅显易懂的介绍进程和线程的文章:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
一、什么是线程
1. 线程是什么
线程是程序执行的一条路径,一个进程中可以包含多条线程,那么这个进程就是多线程的。
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程。
多线程并发执行可以提高程序的效率,可以同时完成多项工作。
比如我打开360,点击“查杀修复”、“电脑清理”和“优化加速”,如下图。
这3个都在运行,这就是多线程。如果是单线程,在运行“查杀修复”时,“电脑清理”和“优化加速”都得等着。等“查杀修复”运行完了,才能运行下一个。
多线程的效率为什么会高?见:https://www.cnblogs.com/shann/p/6851889.html
2. 多线程的应用场景
- 红蜘蛛同时共享屏幕给多个电脑
- 迅雷就是多条线程一起下载
- QQ同时和多个人一起视频
- 服务器同时处理多个客户端请求
二、多线程并行和并发的区别
- 并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行(需要多核CPU)
- 并发是指两个任务都请求执行,而处理器只能接受一个任务,就把两个任务安排轮流执行,由于时间间隔较短,使人感觉两个任务都在运行。
- 比如我跟两个网友聊天,左手操作一个电脑和甲聊天,同时右手用另一台电脑和乙聊天,这就叫并行。
- 如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊,这就叫并发。
Java程序运行原理:
Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的 main 方法。
JVM的启动是多线程的
JVM启动至少启动了垃圾回收线程和主线程,所以JVM是多线程的。
Demo1_Thread.java
package com.wisedu.thread; /** * Created by jkzhao on 1/22/18. */ public class Demo1_Thread { /** * @param args * 证明jvm是多线程的 */ public static void main(String[] args) { for(int i = 0; i < 1000000; i++) { new Demo(); //创造些垃圾出来,匿名对象就是垃圾 } for(int i = 0; i < 100000; i++) { //看看主线程和垃圾回收线程是否会在cpu上切换 System.out.println("我是主线程的执行代码"); } } } class Demo { @Override public void finalize() { //Object类中的方法 System.out.println("垃圾被清扫了"); } }
“我是主线程的执行代码”和“垃圾被清扫了”这两句话会交替打印,说明是间隔执行的,是多线程的。如果不是多线程的,一定是全部打印完了“垃圾被清扫了”,才会去打印“我是主线程的执行代码”。
三、多线程程序实现方式
1. 继承Thread
【步骤】:
- 定义类继承Thread
- 重写run方法
- 把新线程要做的事情写在run方法中
- 创建线程对象
- 开启新线程,内部会自动执行run方法
【示例】:Demo2_Thread.java
package com.wisedu.thread; /** * Created by jkzhao on 1/22/18. */ public class Demo2_Thread { /** * @param args */ public static void main(String[] args) { //执行main方法,发现有b有a间隔输出了 MyThread mt = new MyThread(); //4,创建Thread类的子类对象 mt.start(); //5,开启线程 注意这里不是调用run方法 for(int i = 0; i < 1000; i++) { //为了看出是多线程执行,在主方法里也写点内容 System.out.println("bb"); } } } class MyThread extends Thread { //1,继承Thread public void run() { //2,重写run方法 for(int i = 0; i < 1000; i++) { //3,将要执行的代码写在run方法中 System.out.println("aaaaaaaaaaaa"); } } }
2. 实现Runnable
【步骤】:
- 定义类实现Runnable接口
- 实现run方法
- 把新线程要做的事情写在run方法中
- 创建自定义的Runnable的子类对象
- 创建Thread对象,传入Runnable
- 调用start()开启新线程,内部会自动调用Runnable的run()方法
【示例】:Demo3_Thread.java
package com.wisedu.thread; /** * Created by jkzhao on 1/22/18. */ public class Demo3_Thread { /** * @param args */ public static void main(String[] args) { MyRunnable mr = new MyRunnable(); //4,创建Runnable的子类对象 //Runnable target = mr; 假如mr = 0x0011 //父类引用指向子类对象,编译看的是父类,运行看的是子类 Thread t = new Thread(mr); //5,将其当作参数传递给Thread的构造函数 t.start(); //6,开启线程 Thread类中才有start方法 for(int i = 0; i < 1000; i++) { System.out.println("bb"); } } } class MyRunnable implements Runnable { //1,定义一个类实现Runnable @Override public void run() { //2,重写run方法 for(int i = 0; i < 1000; i++) { //3,将要执行的代码写在run方法中 System.out.println("aaaaaaaaaaaa"); } } }
3. 实现Runnable的原理
查看源码,可以发现:
- 看Thread类的构造方法,传递了Runnable接口的引用
- 通过init()方法找到传递的target给成员变量的target赋值
- 查看run方法,发现run方法中有判断,如果target不为null就会调用Runnable接口子类对象的run方法
4. 两种实现多线程方式的区别
查看源码的区别:
- 继承Thread:由于子类重写了Thread类的run(),当调用start()时,可以找子类的run()方法(调用start方法时,找子类的run方法,这是底层JVM帮我们完成的)
- 实现Runnable:构造方法中传入了Runnable的引用,成员变量记住了它,start()方法调用run()方式时内部判断成员变量Runnable的引用是否为空,不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法。
继承Thread:
- 好处是:可以直接使用Thread类中的方法,代码简单
- 弊端是:如果已经有了父类,就不能用这种方法
实现Runnable:
- 好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的
- 弊端是:不能直接使用Thread中的方法,需要先获取到线程对象后,才能得到Thread的方法,代码复杂。
5. 匿名内部类实现线程的两种方式
好处就是不用找一个类去继承Thread类或者实现Runnable接口了。
【示例】:Demo4_Thread.java
package com.wisedu.thread; /** * Created by jkzhao on 1/22/18. */ public class Demo4_Thread { /** * @param args */ public static void main(String[] args) { new Thread() { //1,继承Thread类 public void run() { //2,重写run方法 for(int i = 0; i < 1000; i++) { //3,将要执行的代码写在run方法中 System.out.println("aaaaaaaaaaaaaa"); } } }.start(); //4,开启线程 new Thread(new Runnable() { //1,将Runnable的子类对象传递给Thread的构造方法 public void run() { //2,重写run方法 for(int i = 0; i < 1000; i++) { //3,将要执行的代码写在run方法中 System.out.println("bb"); } } }).start(); //4,开启线程 } }
四、多线程中的一些方法
1. 获取名字和设置名字
(1)获取名字
通过getName()方法获取线程对象的名字
(2)设置名字
通过构造函数可以传入String类型的名字
通过setName(String )方法可以设置线程对象的名字
【示例】:Demo1_Name.java
package com.wisedu.threadmethod; /** * Created by jkzhao on 1/22/18. */ public class Demo1_Name { /** * @param args */ public static void main(String[] args) { //demo1(); Thread t1 = new Thread() { public void run() { //this.setName("张三"); System.out.println(this.getName() + "....aaaaaaaaaaaaa"); //this就相当于这个匿名内部类对象 } }; Thread t2 = new Thread() { public void run() { //this.setName("李四"); System.out.println(this.getName() + "....bb"); } }; t1.setName("张三"); t2.setName("李四"); t1.start(); t2.start(); } public static void demo1() { new Thread("芙蓉姐姐") { //通过构造方法给name赋值 public void run() { System.out.println(this.getName() + "....aaaaaaaaa"); } }.start(); new Thread("凤姐") { public void run() { System.out.println(this.getName() + "....bb"); } }.start(); } }
2. 获取当前线程的对象
Thread.currentThread(),主线程也可以获取。
【示例】:Demo2_CurrentThread.java
package com.wisedu.threadmethod; /** * Created by jkzhao on 1/22/18. */ public class Demo2_CurrentThread { /** * @param args */ public static void main(String[] args) { new Thread() { public void run() { System.out.println(getName() + "....aaaaaa"); } }.start(); new Thread(new Runnable() { public void run() { //Thread.currentThread()获取当前正在执行的线程 System.out.println(Thread.currentThread().getName() + "...bb"); } }).start(); Thread.currentThread().setName("我是主线程"); System.out.println(Thread.currentThread().getName()); } }
3. 休眠线程
Thread.sleep(毫秒,纳秒),控制当前线程休眠若干毫秒,windows不太支持纳秒值。 1秒 = 1000毫秒,1秒 = 1000 * 1000 * 1000m纳秒。
【示例】:Demo3_Sleep.java
package com.wisedu.threadmethod; /** * Created by jkzhao on 1/22/18. */ public class Demo3_Sleep { /** * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { //demo1(); new Thread() { public void run() { for(int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + "...aaaaaaaaaa"); } } }.start(); new Thread() { public void run() { for(int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + "...bb"); } } }.start(); } public static void demo1() throws InterruptedException { for(int i = 20; i >= 0; i--) { Thread.sleep(1000); //毫秒 System.out.println("倒计时第" +i + "秒"); } } }
4. 守护线程
setDaemon(),设置一个线程为守护线程,该线程不会单独执行,当其他非守护线程都执行结束后,自动退出。
就像下象棋一样,非守护线程相当于帅,守护线程相当于车马相士。帅死掉了,其他的车马相士也随之停止,车马相士自杀有个缓冲时间。
再比如,用QQ向别人传文件的时候,qq的主界面用的是非守护线程做的,而传输的窗口用的守护线程,主界面一关掉,传输并不是立马就停止的,它还会再发几个数据包,因为它要等待接收退出的命令。
【示例】:Demo4_Daemon.java
package com.wisedu.threadmethod; /** * Created by jkzhao on 1/22/18. */ public class Demo4_Daemon { /** * @param args * 守护线程 */ public static void main(String[] args) { Thread t1 = new Thread() { //帅 public void run() { for(int i = 0; i < 2; i++) { System.out.println(getName() + "...aaaaaaaaaaaaaaaaaaaa"); } } }; Thread t2 = new Thread() { //车马相士 public void run() { for(int i = 0; i < 50; i++) { System.out.println(getName() + "...bb"); } } }; t2.setDaemon(true); //当传入true,意味着设置为守护线程 t1.start(); t2.start(); } }
5. 加入线程
- join(),当前线程暂停,等待指定的线程执行结束后,当前线程再继续。就相当于插队。
- join(int),可以等待指定的毫秒之后继续
【示例】:Demo5_Join.java
package com.wisedu.threadmethod; /** * Created by jkzhao on 1/22/18. */ public class Demo5_Join { /** * @param args * join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续 */ public static void main(String[] args) { final Thread t1 = new Thread() { public void run() { for(int i = 0; i < 10; i++) { System.out.println(getName() + "...aaaaaaaaaaaaa"); } } }; Thread t2 = new Thread() { public void run() { for(int i = 0; i < 10; i++) { if(i == 2) { try { //t1.join(); //插队线程执行完后,当前线程才能执行。这里匿名内部类在使用它所在方法中的局部变量的时候,该变量必须用final修饰 t1.join(1); //插队指定的时间,过了指定时间后,两条线程交替执行 } catch (InterruptedException e) { //插队过来,当前线程出现中断异常 e.printStackTrace(); } } System.out.println(getName() + "...bb"); } } }; t1.start(); t2.start(); } }
6. 礼让线程
yield让出CPU,但是这个yield实现的效果不好,会达不到这个效果。
【示例】:Demo6_Yield.java
package com.wisedu.threadmethod; /** * Created by jkzhao on 1/22/18. */ public class Demo6_Yield { /** * yield让出cpu,礼让线程 */ public static void main(String[] args) { new MyThread().start(); new MyThread().start(); } } class MyThread extends Thread { public void run() { for(int i = 1; i <= 1000; i++) { System.out.println(getName() + "..." + i); if(i % 10 == 0) { //10的倍数 Thread.yield(); //让出CPU } } } }
7. 设置线程优先级
setPriority() 设置线程优先级
【示例】:Demo7_Priority.java
package com.wisedu.threadmethod; /** * Created by jkzhao on 1/22/18. */ public class Demo7_Priority { /** * @param args */ public static void main(String[] args) { Thread t1 = new Thread(){ public void run() { for(int i = 0; i < 100; i++) { System.out.println(getName() + "...aaaaaaaaa" ); } } }; Thread t2 = new Thread(){ public void run() { for(int i = 0; i < 100; i++) { System.out.println(getName() + "...bb" ); } } }; //t1.setPriority(10); //t2.setPriority(1); //直接给值也可以,但是不能超过下面两个常量 t1.setPriority(Thread.MIN_PRIORITY); //设置最小的线程优先级 t2.setPriority(Thread.MAX_PRIORITY); //设置最大的线程优先级 t1.start(); t2.start(); } }
Thread.interrupted()
待补充。。。
五、多线程之线程同步
1. 什么情况下需要同步
当多线程并发,有多段代码同时执行时,我们希望某一段代码执行的过程中CPU不要切换到其他线程工作,等当前这段代码执行完再切换到其它线程,这时就需要同步。
如果两段代码是同步的,那么同一个时间只能执行一段,在一段代码没执行结束之前,不会执行另外一段代码。
举个例子:赚了3000块钱存银行,然后你有一存款折和一张银行卡,都是对应于同一个账户,你拿这个折子去柜台取钱去,柜台服务人员问你取多少钱,你说2000。这个时候服务人员得把2000输入电脑,电脑会去检查你现在有没有2000块钱。一查发现你有多余2000块的存款,这时候就把钱吐给你。然后把你账户上的钱减掉2000。我们假设检查完你账户发现钱够,正要把钱出给你的时候,你老婆拿着你的银行卡在取款机上取钱,取2000,取款机一检查发现你的账户里有超过2000的存款(此时你的账户上钱还没减),然后取款机就把这2000吐给你老婆了,然后取款机把你的账户的余额更新为1000。然后柜台那边继续执行,又给了你2000,把你的账户余额更新为1000。
这显然对银行不公平,你和你老婆好比两个线程,这两个线程在执行一个对账户取款的方法的过程之中,但是你们两个线程同时访问同一个账户(同一个资源),这种情况下线程顺序协调不好的话,很容易出现前后数据不一致的情况。我们对多个线程访问同一个资源的时候,我们对这多个线程进行协调的这个东西叫做线程同步。
怎么解决这个问题呢?
当某个线程在调用取款方法的时候,这个账户归这个线程独占,其他线程不能访问。
代码模拟:
public class TestSync implements Runnable { Timer timer = new Timer(); public static void main(String[] args) { TestSync test = new TestSync(); Thread t1 = new Thread(test); Thread t2 = new Thread(test); t1.setName("t1"); t2.setName("t2"); t1.start(); t2.start(); } public void run(){ timer.add(Thread.currentThread().getName()); } } class Timer{ private int num = 0; public void add(String name){ //用来做计数用的 num ++; try { Thread.sleep(1); } catch (InterruptedException e) {} System.out.println(name+", 你是第"+num+"个使用timer的线程"); } }
运行结果:
t1, 你是第2个使用timer的线程 t2, 你是第2个使用timer的线程 Process finished with exit code 0
问题出在执行 num ++ 和 打印语句 之间被打断了(因为第一个线程睡眠了),第二个线程调add方法去修改同一个对象的成员变量num的值,接着第二个线程睡眠,第一个线程醒了过来,打印num的值为2。然后第2个线程醒过来打印num的值为2。
num ++ 和 打印语句 应作为原子操作,不可再分。
怎么解决呢?锁住当前对象。
class Timer{ private int num = 0; public void add(String name){ //用来做计数用的 synchronized (this) { //锁定当前对象,对于下面的语句,一个线程在执行的时候不会被另外的线程打断。这个对象里面的成员变量num也被锁定了。这叫互斥锁 num++; try { Thread.sleep(1); } catch (InterruptedException e) { } System.out.println(name + ", 你是第" + num + "个使用timer的线程"); } } }
还有一种更为简洁的写法:
class Timer{ private int num = 0; public synchronized void add(String name){ //在执行该方法时锁定当前对象,也就是timer对象 //synchronized (this) { //锁定当前对象,对于下面的语句,一个线程在执行的时候不会被另外的线程打断。这个对象里面的成员变量num也被锁定了。这叫互斥锁 num++; try { Thread.sleep(1); } catch (InterruptedException e) { } System.out.println(name + ", 你是第" + num + "个使用timer的线程"); //} } }
2. 同步的机制:每个对象都有的方法
synchronized, wait, notify 是任何对象都具有的同步工具。他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。
wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。
当某代码并不持有监视器的使用权时(如图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。
用法如下:
- synchronized单独使用:
- 同步代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容
-
public class Thread1 implements Runnable { Object lock; public void run() { synchronized(lock){ ..do something } } }
- 直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。
-
public class Thread1 implements Runnable { public synchronized void run() { ..do something } }
- synchronized, wait, notify结合:典型场景生产者消费者问题
-
/** * 生产者生产出来的产品交给店员 */ public synchronized void produce() { if(this.product >= MAX_PRODUCT) { try { wait(); System.out.println("产品已满,请稍候再生产"); } catch(InterruptedException e) { e.printStackTrace(); } return; } this.product++; System.out.println("生产者生产第" + this.product + "个产品."); notifyAll(); //通知等待区的消费者可以取出产品了 } /** * 消费者从店员取产品 */ public synchronized void consume() { if(this.product <= MIN_PRODUCT) { try { wait(); System.out.println("缺货,稍候再取"); } catch (InterruptedException e) { e.printStackTrace(); } return; } System.out.println("消费者取走了第" + this.product + "个产品."); this.product--; notifyAll(); //通知等待去的生产者可以生产产品了 }
-
下面将进行更详细的讲解。
3. 同步代码块
使用synchronized关键字加上一个锁对象来定义一段代码,这就叫同步代码块。
多个同步代码块如果使用相同的锁对象,那么它们就是同步的。
【示例】:Demo1_Synchronized.java
package com.wisedu.sync; /** * Created by jkzhao on 1/22/18. */ public class Demo1_Synchronized { /** * @param args * 同步代码块 */ public static void main(String[] args) { final Printer p = new Printer(); new Thread() { public void run() { while(true) { p.print1(); } } }.start(); new Thread() { public void run() { while(true) { p.print2(); } } }.start(); } } class Printer { Demo d = new Demo(); public void print1() { //synchronized(new Demo()) { //同步代码块,锁机制,锁是对象来做的,锁对象可以是任意的,直接创建个Object对象也可以 synchronized(d) { System.out.print("黑"); System.out.print("马"); System.out.print("程"); System.out.print("序"); System.out.print("员"); System.out.print("\\r\\n"); } } public void print2() { //synchronized(new Demo()) { //锁对象不能用匿名对象,因为匿名对象不是同一个对象 synchronized(d) { System.out.print("传"); System.out.print("智"); System.out.print("播"); System.out.print("客"); System.out.print("\\r\\n"); } } } class Demo{}
4. 多线程同步方法
使用synchronized关键字修饰一个方法,该方法中所有的代码都是同步的。
【示例】:Demo2_Synchronized.java
package com.wisedu.sync; /** * Created by jkzhao on 1/22/18. */ public class Demo2_Synchronized { /** * @param args * 同步代码块 */ public static void main(String[] args) { final Printer2 p = new Printer2(); new Thread() { public void run() { while(true) { p.print1(); //p.print3(); } } }.start(); new Thread() { public void run() { while(true) { p.print2(); //p.print4(); } } }.start(); } } class Printer2 { //非静态的同步方法的锁对象是神马? //答:非静态的同步方法的锁对象是this public synchronized void print1() { //同步方法只需要在方法上加synchronized关键字即可 System.out.print("黑"); System.out.print("马"); System.out.print("程"); System.out.print("序"); System.out.print("员"); System.out.print("\\r\\n"); } public void print2() { synchronized(this) { System.out.print("传"); System.out.print("智"); System.out.print("播"); System.out.print("客"); System.out.print("\\r\\n"); } } //静态的同步方法的锁对象是什么? //答:是该类的字节码对象 public static synchronized void print3() { //同步方法只需要在方法上加synchronized关键字即可 System.out.print("黑"); System.out.print("马"); System.out.print("程"); System.out.print("序"); System.out.print("员"); System.out.print("\\r\\n"); } public static void print4() { synchronized(Printer2.class) { System.out.print("传"); System.out.print("智"); System.out.print("播"); System.out.print("客"); System.out.print("\\r\\n"); } } }
六、线程安全
多线程并发操作同一数据时,就有可能出现线程安全问题。
使用同步技术可以解决这种问题,把操作数据的代码进行同步,不要多个线程一起操作。
【示例】:铁路售票,一共100张,通过四个窗口卖完(四个窗口也就是四个线程)。
Demo3_Ticket.java --继承Thread类
package com.wisedu.sync; /** * Created by jkzhao on 1/22/18. */ public class Demo3_Ticket { /** * 需求:铁路售票,一共100张,通过四个窗口卖完. */ public static void main(String[] args) { new Ticket().start(); new Ticket().start(); new Ticket().start(); new Ticket().start(); } } class Ticket extends Thread { private static int ticket = 100; //所有对象共享这100张票 //private static Object obj = new Object(); //如果用 引用数据类型的成员变量 当作锁对象,必须是静态的 public void run() { while(true) { synchronized(Ticket.class) { //obj if(ticket == 0) { break; } try { //模拟这边可能有n多行代码执行 Thread.sleep(10); //10毫秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + "...这是第" + ticket-- + "号票"); } } } }
Demo4_Ticket.java --实现Runnable接口
package com.wisedu.sync; /** * Created by jkzhao on 1/22/18. */ public class Demo4_Ticket { /** * @param args * 火车站卖票的例子用实现Runnable接口 */ public static void main(String[] args) { MyTicket mt = new MyTicket(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); /*Thread t1 = new Thread(mt); //多次启动一个线程是非法的 t1.start(); t1.start(); t1.start(); t1.start();*/ } } class MyTicket implements Runnable { private int tickets = 1000; //因为我们不需要创建4个对象,所以不需要定义成静态的 @Override public void run() { while(true) { synchronized(Ticket.class) { //可以用this,因为只创建了一个对象 if(tickets == 0) { break; } try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "...这是第" + tickets-- + "号票"); } } } }
七、多线程的死锁
多线程同步的时候,如果代码嵌套,使用相同锁,就有可能出现死锁。所以尽量不要嵌套使用。
比如,桌子上摆了一桌满汉全席,桌子周围坐着一群哲学家,然后给哲学家发筷子,一人一根,然后哲学家就用自己的三寸不烂之舌说服别人把筷子给自己。互相去说服,最后可能导致谁都没有说服谁,导致成了死锁,全部活活饿死在满汉全席的边上。
【示例】:Demo5_DeadLock.java
package com.wisedu.sync; /** * Created by jkzhao on 1/22/18. */ public class Demo5_DeadLock { /** * @param args */ private static String s1 = "筷子左"; //定义两个字符串当做锁 private static String s2 = "筷子右"; public static void main(String[] args) { new Thread() { public void run() { while(true) { synchronized(s1) { System.out.println(getName() + "...获取" + s1 + "等待" + s2); synchronized(s2) { System.out.println(getName() + "...拿到" + s2 + "开吃"); } } } } }.start(); new Thread() { public void run() { while(true) { synchronized(s2) { System.out.println(getName() + "...获取" + s2 + "等待" + s1); synchronized(s1) { System.out.println(getName() + "...拿到" + s1 + "开吃"); } } } } }.start(); } }
执行结果:
Thread-0...获取筷子左等待筷子右 Thread-0...拿到筷子右开吃 Thread-0...获取筷子左等待筷子右 Thread-0...拿到筷子右开吃 Thread-0...获取筷子左等待筷子右 Thread-0...拿到筷子右开吃 Thread-0...获取筷子左等待筷子右 Thread-1...获取筷子右等待筷子左
此时已经产生死锁了,程序并没有停止。。。
八、线程安全的类
Vertor、StringBuffer、Hashtable、Collections.synchronized(xxx)
1. Vector是线程安全的
查看源码java.util.Vector.java,找到add方法
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
2. ArrayList是线程不安全的
查看源码,java.util.ArrayList.java,找到add方法
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
3. StringBuffer是线程安全的,StringBuilder是线程不安全的
查看源码,java.lang.StringBuffer,java.lang.StringBuilder,找到append方法
public synchronized StringBuffer append(Object obj) { super.append(String.valueOf(obj)); return this; } public synchronized StringBuffer append(String str) { super.append(str); return this; }
...
public StringBuilder append(Object obj) { return append(String.valueOf(obj)); } public StringBuilder append(String str) { super.append(str); return this; } ...
4. Hashtable是线程安全的,HashMap是线程不安全的
查看源码,java.lang.Hashtable,java.lang.HashMap,找到append方法
java.lang.Hashtable
public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } ...
java.lang.HashMap
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } ...
5. Collections.synchronized(xxx)
可以将线程不安全的线程变成线程安全的。可以到API文档里搜索下Collections这个类,里面有方法synchronized(xxx)
调用上面的这些方法就可以将这几种集合变成同步的。
九、多线程设计模式之单例设计模式
单例设计模式:保证类在内存中只有一个对象
如何保证类在内存中只有一个对象?
- 控制类的创建,不让其他类来创建本类的对象。private
- 在本类中定义一个本类的对象。Singleton s;
- 提供公共的访问方式,public static Singleton getInstance(){return s;}
1. 单例模式两种写法
(1)饿汉式,开发使用这种方式
(2)懒汉式,面试写这种方式。多线程问题?
Demo1_Singleton.java
package com.wisedu.thread2; /** * Created by jkzhao on 1/23/18. */ public class Demo1_Singleton { /** * @param args * * 单例设计模式:保证类在内存中只有一个对象。 */ public static void main(String[] args) { Singleton s1 = Singleton.s; //成员变量被私有,不能通过类名.调用 //Singleton.s = null; //修改成员变量s Singleton s2 = Singleton.s; System.out.println(s1 == s2); /* Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2);*/ } } /* * 饿汉式 * 上来就是new对象,所以称为饿汉式 private static Singleton s = new Singleton(); * / /*class Singleton { //1,私有构造方法,其他类不能访问该构造方法了 private Singleton(){} //2,创建本类对象 private static Singleton s = new Singleton(); //私有化是为了防止被修改 //3,对外提供公共的访问方法 public static Singleton getInstance() { //由于上面把成员变量s私有化,这里提供get方法获取实例 return s; } }*/ /* * 懒汉式,单例的延迟加载模式 */ /*class Singleton { //1,私有构造方法,其他类不能访问该构造方法了 private Singleton(){} //2,声明一个引用 private static Singleton s ; //3,对外提供公共的访问方法 public static Singleton getInstance() { //获取实例 if(s == null) { //假设线程1刚进来,结果CPU执行权被别的线程抢走了,那么线程1等待,线程2刚进来,CPU正好也切换去执行其他线程了,线程2等待。 //然后线程1抢回了CPU,于是执行下面语句创建了一个对象;线程2也抢回了CPU,于是执行下面语句又创建了一个对象。这就创建了两个对象 //所以开发的时候不用懒汉式,面试的时候用这个懒汉式,因为面试的时候会让写一个单例的延迟加载模式 s = new Singleton(); } return s; } }*/ /* * 饿汉式和懒汉式的区别 * 1,饿汉式是空间换时间,懒汉式是时间换空间 * 2,在多线程访问时,饿汉式不会创建多个对象,而懒汉式有可能会创建多个对象 */ /** * 第三种,没有名字 */ class Singleton { //1,私有构造方法,其他类不能访问该构造方法了 private Singleton(){} //2,声明一个引用 public static final Singleton s = new Singleton(); }
2. 单例设计模式应用场景之Runtime类
饿汉式
Demo2_Runtime.java
package com.wisedu.thread2; /** * Created by jkzhao on 1/23/18. */ import java.io.IOException; public class Demo2_Runtime { /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { Runtime r = Runtime.getRuntime(); //获取运行时对象 //r.exec("shutdown -s -t 300"); //在单独的进程中执行指定的字符串命令 r.exec("shutdown -a"); //为什么使用单例设计模式? // 第一条代码设置了5min后关机,第二条代码取消关机,修改的是第一条语句修改后的结果 } }
3. 单例设计模式应用场景之Timer类
Timer类,计时器。一种工具,线程用其安排以后在后台线程中执行的任务,可以安排任务执行一次,或者定期重复执行。
与每个Timer对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。
Demo3_Timer.java
package com.wisedu.thread2; /** * Created by jkzhao on 1/23/18. */ import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class Demo3_Timer { /** * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { Timer t = new Timer(); //在指定时间安排指定任务 //第一个参数,是安排的任务,第二个参数是执行的时间,第三个参数是过多长时间再重复执行 t.schedule(new MyTimerTask(), new Date(118, 0, 22, 16, 39, 51),3000); //year是要-1900 最后的3000是毫秒值 while(true) { Thread.sleep(1000); System.out.println(new Date()); //隔1s打一次时间,看看到指定时间是否执行任务 } } } class MyTimerTask extends TimerTask { @Override public void run() { System.out.println("起床背英语单词"); } }
十、线程通信
1. 什么时候需要通信
多个线程并发执行时,在默认情况下CPU是随机切换线程的
如果我们希望它们有规律的执行,就可以使用通信,例如每个线程执行一次打印
2. 两个线程间通信
- 如果需要线程等待,就调用wait()
- 如果希望唤醒等待的线程,就调用notify():唤醒此对象监听器上等待的单个线程
- 这两个方法必须在同步代码中执行,并且使用同步锁对象来调用
Demo1_Notify.java
package com.wisedu.thread_communication; /** * Created by jkzhao on 1/24/18. */ public class Demo1_Notify { /** * @param args * 等待唤醒机制 */ public static void main(String[] args) { final Printer p = new Printer(); new Thread() { public void run() { while(true) { try { p.print1(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); new Thread() { public void run() { while(true) { try { p.print2(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } } //等待唤醒机制 class Printer { private int flag = 1; public void print1() throws InterruptedException { synchronized(this) { if(flag != 1) { this.wait(); //当前线程等待,没有人唤醒它的话,就一直在等待 } System.out.print("黑"); System.out.print("马"); System.out.print("程"); System.out.print("序"); System.out.print("员"); System.out.print("\\r\\n"); flag = 2; this.notify(); //随机唤醒单个等待的线程(假如此时没有等待的线程,随机唤醒下也是可以的) } } public void print2() throws InterruptedException { synchronized(this) { if(flag != 2) { this.wait(); } System.out.print("传"); System.out.print("智"); System.out.print("播"); System.out.print("客"); System.out.print("\\r\\n"); flag = 1; this.notify(); } } }
3. 三个或三个以上间的线程通信
多个线程通信的问题
- notify()方法是随机唤醒此对象监听器上等待的一个线程
- notifyAll()方法是唤醒此对象监听器上等待的所有线程
- JDK5之前无法唤醒指定的一个线程
- 如果多个线程之间通信,需要使用notifyAll()通知所有线程,用while来反复判断条件
Demo2_NotifyAll.java
package com.wisedu.thread_communication; /** * Created by jkzhao on 1/24/18. */ public class Demo2_NotifyAll { /** * 循环打印:黑马程序员、传智播客、itheima * @param args */ public static void main(String[] args) { final Printer2 p = new Printer2(); new Thread() { public void run() { while(true) { try { p.print1(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); new Thread() { public void run() { while(true) { try { p.print2(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); new Thread() { public void run() { while(true) { try { p.print3(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } } /* 1,在同步代码块中,用哪个对象锁,就用哪个对象去调用wait方法,比如下面用的是this * 2,为什么wait方法和notify方法定义在Object这类中? * 因为锁对象可以是任意对象,Object是所有类的基类,所以wait方法和notify方法需要定义在Object这个类中 * 3,sleep方法和wait方法的区别? * a,sleep方法必须传入参数,参数就是时间,时间到了自动醒来 * wait方法可以传入参数也可以不传入参数,传入参数就是在参数的时间结束后等待,不传入参数就是直接等待 * b,sleep方法在同步函数或同步代码块中,不释放锁,睡着了也抱着锁睡(就是在sleep的时间内,也占着CPU) * wait方法在同步函数或者同步代码块中,释放锁(就是该线程调用wait方法等待了,把锁释放了,这样CPU才能去执行其他线程) */ class Printer2 { private int flag = 1; public void print1() throws InterruptedException { synchronized(this) { while(flag != 1) { this.wait(); //当前线程等待 } System.out.print("黑"); System.out.print("马"); System.out.print("程"); System.out.print("序"); System.out.print("员"); System.out.print("\\r\\n"); flag = 2; //this.notify(); //不能用这个 this.notifyAll(); } } public void print2() throws InterruptedException { synchronized(this) { while(flag != 2) { this.wait(); //线程2在此等待 } System.out.print("传"); System.out.print("智"); System.out.print("播"); System.out.print("客"); System.out.print("\\r\\n"); flag = 3; //this.notify(); //不能用这个 this.notifyAll(); } } public void print3() throws InterruptedException { synchronized(this) { while(flag != 3) { this.wait(); //线程3在此等待,if语句是在哪里等待,就在哪里起来 //while循环是循环判断,每次抢到执行权都会去判断标记flag } System.out.print("i"); System.out.print("t"); System.out.print("h"); System.out.print("e"); System.out.print("i"); System.out.print("m"); System.out.print("a"); System.out.print("\\r\\n"); flag = 1; //this.notify(); //不能用这个 this.notifyAll(); } } }
分析下执行过程:
- 极端情况下,上来线程2抢到CPU执行权,CPU先执行线程2,此时flag=1,所以条件满足,线程2等待;
- 接着执行线程3,此时flag=1,所以条件满足,线程3等待;
- 现在活着的只有线程1,执行线程1,此时flag=1,条件不满足,所以线程1的代码继续往下走,开始打印,打印完将flag改为2,然后通过notifyAll()唤醒所有等待的线程,很有可能线程1仍然有CPU执行权,于是继续走while里的判断,2!=1,条件满足,线程1等待;
- 现在线程2和线程3都是活着的,假设先执行的线程3,2!=3,条件满足,于是线程3等待;
- 现在活着的只有线程2,2!=2,条件不满足,继续往下走,于是开始打印,然后把flag改为3,然后通过notifyAll()唤醒所有等待的线程。。。
但是这不合理,也就是说不管符合不符合规则,到没到时间,就把其它线程都叫起来。比如说园区有3个保安,一个值早上8点到下午4点的班,一个是下午4点到晚上12点,一个是晚上12点到第二天早上8点的班。第一个值班后并不知道谁盯下一个班,于是就把另外两个保安叫起来了。两个人起来后看谁满足条件就去值班,另一个回去睡觉。如果总这么叫起人的话,人家就可能怒了。
这是JDK 1.5之前的解决方案,在JDK 1.5有个更好的解决方案叫互斥锁。
4. JDK 1.5的新特性之互斥锁
- 同步
- 使用ReentrantLock类的lock()和unlock()方法进行同步
- lock()获取锁
- unlock()释放锁
- 通信
- 使用ReentrantLock类的newCondition()方法可以获取Condition对象
- 需要等待的时候使用Condition的await()方法,唤醒的时候用signal()方法
- 不同的线程使用不同的Condition,这样就能区分唤醒的时候找哪个线程了
Condition 将Object监视器方法(wait、notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition替代了Object监视器方法的使用。Condition实例实质上被绑定到一个锁上,要为特定 Lock 实例获得 Condition 实例,请用其 newCondition() 方法。
Demo3_ReetrantLock.java
package com.wisedu.thread_communication; /** * Created by jkzhao on 1/24/18. */ import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class Demo3_ReentrantLock { /** * @param args */ public static void main(String[] args) { final Printer3 p = new Printer3(); new Thread() { public void run() { while(true) { try { p.print1(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); new Thread() { public void run() { while(true) { try { p.print2(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); new Thread() { public void run() { while(true) { try { p.print3(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } } class Printer3 { private ReentrantLock r = new ReentrantLock(); private Condition c1 = r.newCondition(); //创建3个监视器,一个线程上放一个监视器 private Condition c2 = r.newCondition(); private Condition c3 = r.newCondition(); private int flag = 1; public void print1() throws InterruptedException { r.lock(); //获取锁 if(flag != 1) { c1.await(); //等待 } System.out.print("黑"); System.out.print("马"); System.out.print("程"); System.out.print("序"); System.out.print("员"); System.out.print("\\r\\n"); flag = 2; //this.notify(); c2.signal(); //唤醒 r.unlock(); //释放锁 } public void print2() throws InterruptedException { r.lock(); if(flag != 2) { c2.await(); } System.out.print("传"); System.out.print("智"); System.out.print("播"); System.out.print("客"); System.out.print("\\r\\n"); flag = 3; //this.notify(); c3.signal(); r.unlock(); } public void print3() throws InterruptedException { r.lock(); if(flag != 3) { c3.await(); } System.out.print("i"); System.out.print("t"); System.out.print("h"); System.out.print("e"); System.out.print("i"); System.out.print("m"); System.out.print("a"); System.out.print("\\r\\n"); flag = 1; c1.signal(); r.unlock(); } }
十一、线程组的概述和使用
1. 线程组的概述
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。
默认情况下,所有的线程都属于主线程组。
- public final ThreadGroup getThreadGroup() //通过线程对象获取他所属于的组
- public final String getName() //通过线程组对象获取他所在组的名字
我们也可以给线程设置分组
- ThreadGroup(String name) 创建线程组对象并给其赋值名字
- 创建线程对象
- Thread(ThreadGroup?group, Runnable?target, String?name)
- 设置整组的优先级或守护线程
2. 线程组的使用
【示例】:线程组的使用,默认是主线程组。
Demo4_ThreadGroup.java
package com.wisedu.thread_communication; /** * Created by jkzhao on 1/24/18. */ public class Demo4_ThreadGroup { /** * @param args * ThreadGroup */ public static void main(String[] args) { //demo1(); ThreadGroup tg = new ThreadGroup("我是一个新的线程组"); //创建新的线程组 MyRunnable mr = new MyRunnable(); //创建Runnable的子类对象 Thread t1 = new Thread(tg, mr, "张三"); //将线程t1放在组中 Thread t2 = new Thread(tg, mr, "李四"); //将线程t2放在组中 System.out.println(t1.getThreadGroup().getName()); //获取线程组的名字 System.out.println(t2.getThreadGroup().getName()); tg.setDaemon(true); //整个组内的线程都变成守护线程了 } public static void demo1() { MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr, "张三"); Thread t2 = new Thread(mr, "李四"); ThreadGroup tg1 = t1.getThreadGroup(); ThreadGroup tg2 = t2.getThreadGroup(); System.out.println(tg1.getName()); //打印出来是main,线程默认的是在主线程组 System.out.println(tg2.getName()); } } class MyRunnable implements Runnable { @Override public void run() { for(int i = 0; i < 1000; i++) { System.out.println(Thread.currentThread().getName() + "...." + i); } } }
十二、线程的六种状态
其中,stop()方法太暴力,已过时了。在以前通过thread.stop()可以停止一个线程,注意stop()方法是可以由一个线程去停止另外一个线程,这种方法太过暴力而且是不安全的,怎么说呢,线程A调用线程B的stop方法去停止线程B,调用这个方法的时候线程A其实并不知道线程B执行的具体情况,这种突然间地停止会导致线程B的一些清理工作无法完成,还有一个情况是执行stop方法后线程B会马上释放锁,这有可能会引发数据不同步问题。基于以上这些问题,stop()方法被抛弃了。
再来两张图:
线程状态
线程状态转换
各种状态一目了然,值得一提的是"blocked"这个状态:
线程在Running的过程中可能会遇到阻塞(Blocked)情况
- 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
- 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
- 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。
十三、线程池的概述和使用
1. 线程池概述
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。在JDK 5之前,我们必须手动实现自己的线程池,从JDK 5开始,Java内置了线程池。
2. 内置线程池的使用概述
JDK 5新增了一个Executors工厂类来产生线程池,有如下几个方法:
-
public static ExecutorService newFixedThreadPool(int nThreads)
-
public static ExecutorService newSingleThreadExecutor()
- 这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法:
-
Future<?> submit(Runnable task) //提交一个返回值的任务用于执行,返回一个表示任务的未决结果的Future
-
<T> Future<T> submit(Callable<
以上是关于Java多线程的主要内容,如果未能解决你的问题,请参考以下文章
-