1 Day16–多线程01
1.1 程序概念
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
1.2 进程
1.2.1 概念
进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态
的过程:有它自身的产生、存在和消亡的过程。——生命周期
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
注意:一个程序执行可以有多个进程。一个软件的运行都需要依赖进程执行.可能会依赖一个进程(IDEA),也可以依赖多个进程(chrome).软件运行时就加载对应的进程,软件关闭时对应的进程会消失,具有动态性.。
1.3 线程
1.3.1 特点
- 独立性:进程是系统中独立存在的实体,它可以拥有自己的独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
- 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
1.3.2 概念
线程(thread):是操作系统能够进行运算调度的最小单位,是一个程序内部的一条执行路径。它被包含在进程之中,是进程中的实际运作单位。一个进程可以开启多个线程。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 如果一个进程只有一个线程,这种程序被称为单线程。
- 如果一个进程中有多条执行路径被称为多线程程序(如360:可以同时清理垃圾,杀毒,电脑体检等操作。)。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间
- 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
- 简而言之,一个程序运行后至少一个进程,一个进程里包含多个线程。
1.4 进程和线程的关系
图1
从图1中可以看出:一个操作系统中可以有多个进程,一个进程中可以有多个线程,每个进程有自己独立的内存,每个线程共享一个进程中的内存,每个线程又有自己独立的内存。(记清这个关系,非常重要!)
所以想使用线程技术,得先有进程,进程的创建是OS创建的,你能实现吗?不能,一般都是c或者c++语言完成的。
关系:
- 进程的范围大一些,可以包含多个线程.线程的范围小一些,是每个软件运行时能调度的最小单位.
- 进程有自己的存储空间,可以存放自己的数据,可以包含多个线程.
- 线程也有自己的存储空间,每个线程间是独立的.
图2
由图2可以看出:
- 虚拟机栈和程序计数器,它们2个每个线程各有一份。
- 方法区和堆,它们2个每个进程程各有一份。
- 即:假如一个程序有2个进程,每个进程都要一份方法区和堆。每个进程又有2个线程,每个线程又有一份虚拟机栈和程序计数器。所以每个线程共享同一个进程的方法区和堆,以至于多个线程操作共享的系统资源可能就会带来安全的隐患。
1.5 单核/多核CPU,并行/并发的概念
1.5.1 CPU分时调度
时间片:即CPU分配给各个程序的时间,每个进程被分配一个时间段,称作它的时间片。即该进程允许运行的时间,使各个程序从表面上看是同时进行的。
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程,将当前进程挂起。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换,而不会造成CPU资源浪费。当又切换到之前执行的进程,把现场恢复,继续执行。
在宏观上:我们可以同时打开多个应用程序,每个程序并行,同时运行。1/3000ns
在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。多核提高了并发能力。
1.6 多线程
1.6.1 多线程的优点
单核cpu按照先后顺序执行多个任务:因为多线程还需要来回切换需要花费时间,反而单线程比多线程速度更快。
多核cpu执行多个任务:单线程不管几个cpu还是只能一个一个的执行,多线程因为cpu是多个可以来会切换,一个cpu控制一部分线程,多个cpu控制多个线程同时执行任务速度远快于单线程。
1.6.2 何时用多线程
1.6.3 如何区分单线程,多线程
1.7 多线程的特性
1.7.1 随机性
1.7.2 线程状态
线程生命周期,总共有五种状态:
- 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
- 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
- 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态;
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a)等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
b)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
c)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 - 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
1.8 多线程创建1:继承Thread类
1.8.1 概述
Thread类(lang包)本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。Start()方法是一个native方法,它将通知底层操作系统,最终由操作系统启动一个新线程,操作系统将执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
模拟开启多个线程,每个线程调用run()方法
线程是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
1.8.2 创建对象
//自己定义一个子类继承Thread,可以是多态的的形式创建
Thread()
分配新的 Thread 对象。
Thread(Runnable target)
分配新的 Thread 对象。
Thread(Runnable target, String name)
分配新的 Thread 对象。
Thread(String name) //创建对象时指定的线程名称。
分配新的 Thread 对象。
......
1.8.3 Thread常用方法和线程优先级
package atguigu.java;
/**
* 测试Thread中的常用方法:
* 1. void start():启动当前线程;调用当前线程的run()
* 2. void run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
* 3. static Thread currentThread():静态方法,返回执行当前代码的线程 通过类名调用
* 4. String getName():获取当前线程的名字
* 5. void setName():设置当前线程的名字
* 6. static void yield():释放当前cpu的执行权
* 7. void join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才
* 结束阻塞状态。
* 8. void stop():已过时。当执行此方法时,强制结束当前线程。
* 9. static void sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前
* 线程是阻塞状态。
* 10. boolean isAlive():判断当前线程是否存活
* 11.long getId() 返回该线程的标识符。
*
*
* 线程的优先级:
* 1.
* MAX_PRIORITY:10 最大
* MIN _PRIORITY:1 最小
* NORM_PRIORITY:5 -->默认优先级
* 2.如何获取和设置当前线程的优先级:
* int getPriority():获取线程的优先级
* void setPriority(int p):设置线程的优先级
*
* 说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下
* 被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
*
*
* @author shkstart
* @create 2019-02-13 下午 2:26
*/
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
// try {
//sleep方法会抛异常,因为这是重写后的方法,重写之前的方法没有抛异常所以不能用throws只能是try-catch
// sleep(10);//单位:毫秒 1000毫秒=1秒
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
// if(i % 20 == 0){
// yield();
// }
}
}
public HelloThread(String name){
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
HelloThread h1 = new HelloThread("Thread:1");//通过构造器给线程命名
// h1.setName("线程一"); 写在start()方法之前 通过setName方法给线程命名
//设置分线程的优先级 可以写数字 1 2 3...,也可以写这几个值 MAX_PRIORITY MIN _PRIORITY 但只能代表10 1
h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
//给主线程命名
Thread.currentThread().setName("主线程");
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
// if(i == 20){
// try {
// h1.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
}
// System.out.println(h1.isAlive());
}
}
1.8.4 案例测试1:2个线程(t1,t2)干相同的事情
只需要定义一个子类继承Thread即可。
package atguigu.java;
/**
* 多线程的创建,方式一:继承于Thread类
* 1. 创建一个继承于Thread类的子类
* 2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中
* 3. 创建Thread类的子类的对象
* 4. 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
* <p>
* 例子:遍历100以内的所有的偶数
*
* @author shkstart
* @create 2019-02-13 上午 11:46
*/
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
//2. 重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
t1.start();
//问题一:我们不能通过直接调用run()的方式启动线程。
// t1.run();
//问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
// t1.start();
//我们需要重新创建一个线程的对象
MyThread t2 = new MyThread();
t2.start();
//如下操作仍然是在main线程中执行的。
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
}
}
}
}
根据线程名可以看出执行结果是随机的
注意:从上面结果可以确认,start()方法只是通知操作系统线程就绪,具体什么时间执行,操作系统来决定,我们JVM已经控制不了了。
1.8.5 案例测试2:2个线程(t1,t2)干不同的事情
需要定义2个子类继承Thread。
package atguigu.exer;
/**
* 练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
*
*
* @author shkstart
* @create 2019-02-13 下午 2:16
*/
public class ThreadDemo {
public static void main(String[] args) {
// MyThread1 m1 = new MyThread1();
// MyThread2 m2 = new MyThread2();
//
// m1.start();
// m2.start();
//如果一个线程只用一次,可以简写为创建Thread类的匿名子类的方式
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
1.9 多线程创建2:实现Runnable接口
1.9.1 概述
如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口。 (接口可以多实现,更灵活)
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。(因为接口中有一个抽象方法run(),所以子类必须重写父类的抽象方法)
1.9.2 常用方法
//只有一个方法
void run()
使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。
1.9.3 测试
package atguigu.java;
/**
* 创建多线程的方式二:实现Runnable接口
* 1. 创建一个实现了Runnable接口的类
* 2. 实现类去实现Runnable中的抽象方法:run()
* 3. 创建实现类的对象
* 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5. 通过Thread类的对象调用start()
*
*
* 比较创建线程的两种方式。
* 开发中:优先选择:实现Runnable接口的方式
* 原因:1. 实现的方式没有类的单继承性的局限性
* 2. 实现的方式更适合来处理多个线程有共享数据的情况。
*
* 联系:public class Thread implements Runnable
* 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
*
* @author shkstart
* @create 2019-02-13 下午 4:34
*/
//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
//2. 实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3. 创建实现类的对象
MThread mThread = new MThread();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
t1.setName("线程1");
//5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
t1.start();
//再启动一个线程,遍历100以内的偶数
Thread t2 = new Thread(mThread);
t2.setName("线程2");
t2.start();
}
}
注意:可以看到执行顺序是乱的,我们已经知道start()方法只是通知操作系统线程就绪,具体什么时间执行,操作系统来决定,我们JVM已经控制不了了。这就是乱序的原因,也是正常的。
1.10 售票案例的线程安全问题
设计4个售票窗口,总计售票100张。
用多线程的程序设计并写出代码。
1.10.1 方案1:继承Thread
package cn.teud.threaddemo;
//测试 卖票:设计4个售票窗口,总计售票100张。
public class Test4_Tickets {
public static void main(String[] args) {
//4,测试
//问题1: 现在4个线程,卖了400张票 ?? --
//原因:tickets是成员变量也叫实例变量,只要创建了实例,每个实例就都会拥有tickets资源,创建了4个实例,就会拥有了4个tickets
//解决:如果能够把tickets作为一个全局唯一的全局共享的资源,从头到尾只会存在一次的资源,那么需要被static修饰.
MyTickets t = new MyTickets();
MyTickets t2 = new MyTickets();
MyTickets t3 = new MyTickets();
MyTickets t4 = new MyTickets();
t.setName("1号窗口");
t2.setName("2号窗口");
t3.setName("3号窗口");
t4.setName("4号窗口");
//5,开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
//模拟多线程售票:extends Thread
class MyTickets extends Thread {
//1,定义变量,记录票数
static int tickets = 100;//因为tickets资源会被每个对象拥有一次,不符合需求.想要在多个对象间共享一个tickets资源,所以static
//2,开始卖票 -- 业务放在重写的run()里
@Override
public void run() {
//super.run();
//3,一直卖票
while (true) {
//1.假设tickets=1 t1 t2 t3 t4四个人都瞒住了判断条件,进去卖票。(谁睡醒没法控制,产生原因及一个人干活后数据还没来得及恢复就被另一个让人拿到了。)
if (tickets > 0) {//有票就可以卖票
//6,让程序休息一下,如果数据仍然是正确的,那么这个多线程程序才是完美的!!88888888888888(即:静态方法 sleep())
try {
//问题2: 超卖: 程序卖出了 -1 0 -2
//问题3: 重卖: 程序把一张票卖给了多个人
//2.原因??? t1 t2 t3 t4四个人都睡着了。
Thread.sleep(10);//让程序休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
// 问题2超卖产生的原因?
//假t1醒了,现在tickets=1,开始执行tickets--,输出1,tickets自减变为0
//t1刚变完,t3醒了,现在tickets=0,开始执行tickets--,输出0,tickets自减变为-1.
// t3刚变完,t4醒了,现在tickets=-1,开始执行tickets--,输出-1,tickets自减变为-2.
// t4刚变完,t2醒了,现在tickets=2,开始执行tickets--,输出-2,tickets自减变为-3.
//问题三重买产生的原因? (与超卖的原因几乎一样,因为过程不可控,不知道谁先醒,回去抢占资源)
//假t1醒了,现在tickets=73,开始执行tickets--,输出73,tickets自减变为72,还没来得及改变。
// t3醒了,现在tickets=73,开始执行tickets--,输出73,tickets自减变为-72. 还没来得及改变。
//t4醒了,现在tickets=-73,开始执行tickets--,输出-73,tickets自减变为72. 还没来得及改变。
// t4刚变完,t2醒了,现在tickets=72,开始执行tickets--,输出72,tickets自减变为71.
System.out.println( getName() + "=" + tickets--);
} else { //没票就结束
break; //死循环的出口!!
}
}
}
}
1.10.2 方案2:实现Runnable
package cn.teud.threaddemo;
//测试 卖票:设计4个售票窗口,总计售票100张。
public class Test5_Tickets2 {
public static void main(String[] args) {
MyTickets2 target = new MyTickets2();
//怎么把目标对象和Thread对象绑定关系
Thread t = new Thread(target) ;
Thread t2 = new Thread(target) ;
Thread t3 = new Thread(target) ;
Thread t4 = new Thread(target) ;
//启动线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
//模拟多线程售票:implements Runnable
class MyTickets2 implements Runnable {
//1,定义变量,记录票数
/*这个地方没有用static为啥卖的还是100张票而不是400张票。因为实现接口,虽然是开了四个窗口,
但是执行的任务都是同一个target对象,只new了一次所以从始至终卖的是100张票。不用修饰static
修饰也是一个对象.*/
int tickets = 100;
//2, 把卖票业务放入重写的run()
@Override
public void run() {
while (true) {//一直卖票
if (tickets>0) {
//一定要让程序睡一会儿,来检查数据是否安全 !!!
try {
//问题1:超卖:程序卖出了0 -1 -2号票
//问题2:重卖:程序把一张票卖给了多个人
//原因??? 解决方案????
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3,获取线程名Thread.currentThread().getName()
System.out.println(Thread.currentThread().getName()+"="+tickets--);
}else{
break ;//死循环的出口!!!
}
}
}
}
1.10.3 线程安全问题
- 每次创建线程对象,都会生成一个tickets变量值是100,创建4次对象就生成了400张票了。不符合需求,怎么解决呢?能不能把tickets变量在每个对象间共享,就保证多少个对象都是卖这100张票。
解决:用静态修饰或者用第二种方式创建线程。 - 产生超卖,-1张、-2张。- -多线程安全问题
- 产生重卖,同一张票卖给多人。- -多线程安全问题
- 多线程安全问题是如何出现的?
当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
常见情况是由于线程的随机性+访问延迟(加上延迟出错概率变高了)。 - 以后如何判断程序有没有线程安全问题?- - 在多线程程序中+有共享数据+多条语句操作共享数据。
- 如何解决线程安全问题? - - 同步锁, Lock锁(jdk1.5新特性)
解释:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
1.10.4 扩展
1 JVM启动是单线程还是多线程?
多线程,最少要启动main线程和GC线程。
2 守护线程
略。
2 Day17–多线程02+同步锁
2.1 同步锁解决线程安全问题:01
把有可能出现问题的代码包起来,一次只让一个线程执行。通过sychronized关键字实现同步。
当多个对象操作共享数据时,可以使用同步锁解决线程安全问题。
- 目前的多线程编程中,出现了资源抢占现象,引发的数据安全隐患。
- 如果能给资源上把锁就好了。谁有钥匙,拿着钥匙开锁使用资源。
- 同步锁就是用来把共享资源给锁起来。保证共享的资源,在同一时刻资源是独占的没人抢。
- 同步锁,本质上是牺牲了效率,保证了共享资源的安全性。
- 同步和异步的区别:
同步:是指同一时刻只能有一个人操作数据,别人只能排队等待。牺牲了效率,提高了安全。(StringBuffer就是加了锁,所以说效率降低,安全性提高了。)
异步:是指同一时刻没人排队,大家一起上一起抢。提高了效率,牺牲了安全。 - 使用synchronized关键字实现同步锁的效果。
- 并发和并行的区别:
并发:一个cpu同时执行多个程序。
并行:有多个cpu(现在电脑都是多核的:多个cpu),一个cpu执行一个程序。
2.1.1 Synchronized语法
同步的方式优缺点:
- 好处:解决了线程的安全问题。
- 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
//方式一:同步代码块
synchronized(同步监视器/锁对象){//锁的代码块,需要指定锁对象,可以是任意对象,但是必须是同一个对象。
* //需要被同步的代码
}
说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求1:多个线程必须要共用同一把锁。
要求2:同步需要两个或者两个以上的线程。
//方式二:同步方法
synchronized public void eat(){
//需要被同步的代码
}
说明:1.如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
2.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
3.要求和方式一几乎相同,锁对象只有一个。
2.1.2 在开发中如何判断是否有多线程并发的安全隐患?
– 在多线程的场景下 + 共享资源,被多条语句操作 >=2
2.1.3 改造售票案例01(同步代码块- ->Thread)
package com.thread;
/**
* 使用同步代码块解决继承Thread类的方式的线程安全问题
*
* 说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
*
*/
public class Test1 {
public static void main(String[] args) {
//4,测试
//问题1: 现在4个线程,卖了400张票 ?? --
//原因:tickets是成员变量也叫实例变量,只要创建了实例,每个实例就都会拥有tickets资源,创建了4个实例,就会拥有了4个tickets
//解决:如果能够把tickets作为一个全局唯一的全局共享的资源,从头到尾只会存在一次的资源,那么需要被static修饰.
MyTickets t = new MyTickets();
MyTickets t2 = new MyTickets();
MyTickets t3 = new MyTickets();
MyTickets t4 = new MyTickets();
t.setName("1号窗口");
t2.setName("2号窗口");
t3.setName("3号窗口");
t4.setName("4号窗口");
//5,开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
//模拟多线程售票:extends Thread
class MyTickets extends Thread {
//1,定义变量,记录票数
static int tickets = 100;//因为tickets资源会被每个对象拥有一次,不符合需求.想要在多个对象间共享一个tickets资源,所以static
//private static Object obj = new Object();//方式一的锁对象:加上static是因为,Thread方式造了多个对象,所以加上static保证锁对象只有一个
//2,开始卖票 -- 业务放在重写的run()里
@Override
public void run() {
//super.run();
//3,一直卖票
while (true) {
/*锁位置找对合理的位置 + 锁对象是同一个对象 !!
* 位置合适:如果while (true) {放在同步锁里面,则程序执行相当于每次执行一个线程,把票卖完后,值执行别的线程,
* 即:一个人把票买完之后其它人才能买票,显然不合适。
* */
/*同步代码块--->extends Thread方式的锁对象:同步代码块锁对象任意
* 方式一:private static Object obj = new Object();
* 随便创建一个对象当做锁对象,因为以继承Thread的方式创建多个线程对象,所以把锁对象设置为静态的保证多个线程之间的锁对象是同一个。
* 方式二(容易错误):因为每次自己new一个对象作为锁对象太麻烦,直接用this代表本类对象作为锁对象更简洁,但是因为继承Thread的方式是创建了多个
* 线程对象,此时this的对象有:t,t2,t3,t4.不满足多个线程之间锁对象是同一个,所以这种方式错误。当然如果只有一个线程对象可以用。
* 方式三:Class clazz = MyTickets.class,MyTickets.class只会加载一次,使用类对象的方式,类对象只会加载一次。
* */
// synchronized (obj){ 方式一的锁对象
// synchronized (this){方式二的锁对象
synchronized (MyTickets.class){
if (tickets > 0) {//有票就可以卖票
//6,让程序休息一下,如果数据仍然是正确的,那么这个多线程程序才是完美的!!
try {
Thread.sleep(10);//让程序休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( getName() + "=" + tickets--);
} else { //没票就结束
break; //死循环的出口!!
}
}
}
}
}
2.1.4 改造售票案例02(同步代码块- ->Runnable)
package com.thread;
/**
* 使用同步代码块解决实现 Runnable接口的方式的线程安全问题
*
*在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
*
*/
public class Test2 {
public static void main(String[] args) {
MyTickets2 target = new MyTickets2();
//怎么把目标对象和Thread对象绑定关系
Thread t1 = new Thread(target) ;
Thread t2 = new Thread(target) ;
Thread t3 = new Thread(target) ;
Thread t4 = new Thread(target) ;
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//模拟多线程售票:implements Runnable
//3,目前程序中有安全隐患 -- 加锁来解决 -- 锁的位置(发生点到问题结束) -- 锁的对象(同一个)
class MyTickets2 implements Runnable {
//1,定义变量,记录票数
int tickets = 100; //不加static 是因为Runnable天然只造了一个对象
//Object obj = new Object();//方式一的锁对象:不加static 是因为Runnable天然只造了一个对象,注意这个锁对象要放在方法外面。
//2, 把卖票业务放入重写的run()
@Override
public void run() {
// Object obj = new Object();方式一的锁对象的错误方式:不能写在里面,使用的每次都是new出来的新对象,可没统一!!在里面创建对象相当于new了4次对象。
while (true) {//一直卖票
//4,锁位置:从问题起点开始--同步代码块,需要同时指定锁对象,可以是任意对象,但是必须是同一个。
//5,加锁后,多个线程来访问这段资源,都得排队访问不再抢着干了。提高了安全,牺牲了效率。
/*
* 同步代码块--->implements Runnable方式的锁对象:同步代码块锁对象任意
* 方式一:随便创建一个对象当做锁对象,因为以implements Runnable的方式创建天然只有一个线程对象,四个线程来了,使用的都是new出来的Object同一个对象
* 所以锁对象不用设置为static静态的,但是要注意不能写在run()方法里面,在里面写相当于每次调用方法使用的每次都是new出来的新对象,不符合多个线程锁对象共享。
* 方式二:因为每次自己new一个对象作为锁对象太麻烦,直接用this代表本类对象作为锁对象更简洁,因为implements Runnable的方式是创建了一个线程对象,所以
* this只代表一个本类对象的引用target,符合规范。
* */
//synchronized (obj){方式一的锁对象 synchronized (new Object())直接new也是方式一的锁对象的错误方式
synchronized (this){//方式二的锁对象:四个线程来了,使用的都本类的对象this
if (tickets>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"="+tickets--);
}else{
break ;//死循环的出口!!!
}
}
}
}
}
2.1.5 改造售票案例03(同步方法- ->Thread)
package com.thread;
/**
* 使用同步方法解决继承Thread类的方式的线程安全问题
*
*关于同步方法的总结:
* 1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
* 2. 非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
*/
public class Test3 {
public static void main(String[] args) {
//4,测试
//问题1: 现在4个线程,卖了400张票 ?? --
//原因:tickets是成员变量也叫实例变量,只要创建了实例,每个实例就都会拥有tickets资源,创建了4个实例,就会拥有了4个tickets
//解决:如果能够把tickets作为一个全局唯一的全局共享的资源,从头到尾只会存在一次的资源,那么需要被static修饰.
MyTickets3 t = new MyTickets3();
MyTickets3 t2 = new MyTickets3();
MyTickets3 t3 = new MyTickets3();
MyTickets3 t4 = new MyTickets3();
t.setName("1号窗口");
t2.setName("2号窗口");
t3.setName("3号窗口");
t4.setName("4号窗口");
//5,开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
//模拟多线程售票:extends Thread
class MyTickets3 extends Thread {
//1,定义变量,记录票数
static int tickets = 100;//因为tickets资源会被每个对象拥有一次,不符合需求.想要在多个对象间共享一个tickets资源,所以static
//2,开始卖票 -- 业务放在重写的run()里
@Override
public void run() {
//super.run();
//3,一直卖票
while (true) {
show();
}
}
//private synchronized void show(){ //同步监视器:t,t2,t3,t4。此种解决方式是错误的,有线程安全问题,锁对象不共享。
private static synchronized void show(){同步监视器:MyTickets3.class 改为静态的方法,锁对象共享。
if (tickets > 0) {//有票就可以卖票
//6,让程序休息一下,如果数据仍然是正确的,那么这个多线程程序才是完美的!!
try {
Thread.sleep(10);//让程序休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "=" + tickets--);
}
}
}
2.1.6 改造售票案例04(同步方法- ->Runnable)
package com.thread;
/**
* 使用同步方法解决实现 Runnable接口的方式的线程安全问题
*
*说明:如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
* 此案例需要修改代码。
*
*/
public class Test4 {
public static void main(String[] args) {
MyTickets4 target = new MyTickets4();
//怎么把目标对象和Thread对象绑定关系
Thread t1 = new Thread(target) ;
Thread t2 = new Thread(target) ;
Thread t3 = new Thread(target) ;
Thread t4 = new Thread(target) ;
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//模拟多线程售票:implements Runnable
//3,目前程序中有安全隐患 -- 加锁来解决 -- 锁的位置(发生点到问题结束) -- 锁的对象(同一个)
class MyTickets4 implements Runnable {
//1,定义变量,记录票数
int tickets = 100; //不加static 是因为Runnable天然只造了一个对象
//2, 把卖票业务放入重写的run()
@Override
public void run() {
while (true) {//一直卖票
show();
}
}
public synchronized void show(){//同步监视器:this
//synchronized (this){还是同步代码块的方式
if (tickets>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"="+tickets--);
}
//}
}
}
2.2 单例设计模式
2.2.1 懒汉式线程安全问题的解决
package com.thread;
class Single{
// 1、私有化构造方法,不让外界直接new
private Single() {}
// 2、在类的内部,创建好对象
//static :静态只能调用静态
static private Single s = null;
// 3、对外界提供一个公共的get(),返回一个已经准备好的对象
//static是为了外界不通过对象访问而是通过类名直接方法
static public Single get(){
// static synchronized public Single get(){//1.同步方法
/* 方式一:效率低,因为要求只创建一个对象,现在执行顺序是:第一次第一个线程进来,判断对象为null后创建好对象再返回对象。
*之后别的线程也要一个个的进来判断不为空后再返回,每次线程来都要进行判断,效率低。正确做法应该是第一个线程首次创建好对象后
* 挂个牌子已经创建好一个对象了,别的线程不用在进行判断是否有对象而是直接返回对象即可。
*
* */
// synchronized (Single.class) {//2.同步代码块
// if(s==null){
// /*会有线程安全问题:假如现在多个线程,每个线程调用run方法,各个run方法有调用这个get方法。
// 当第一个线程进入到这个get方法后,第一次进入对象为空,此时可能发生阻塞第二个线程进来判断为空
// 也需要创建对象。这样就创建了2个对象不合理。s相当于共享数据。
// */
// s = new Single();
// }
// return s;//是null说明还没有创建对象,不是null说明创建好了对象直接返回,可以保证只创建一次对象。
// }
//方式二:效率稍高
if(s == null){
synchronized (Single.class) {
if(s==null){
s = new Single();
}
}
}
return s;
}
}
2.3 线程的死锁问题
2.3.1 案例测试
package com.atguigu.java1;
/**
* 演示线程的死锁问题
*
* 1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,
* 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
*
* 2.说明:
* 1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
* 2)我们使用同步时,要避免出现死锁。
*
* @author shkstart
* @create 2019-02-15 下午 3:20
*/
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程安全问题:拿着s1等着执行s2
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程安全问题:拿着s2等着执行s1
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
2.4 Lock锁解决线程安全问题:02
JDK5.0新增
2.4.1 Lock锁改造售票案例
package com.atguigu.java1;
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全问题的方式三:Lock锁 --- JDK5.0新增
*
* 1. 面试题:synchronized 与 Lock的异同?
* 相同:二者都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
*
* 2.优先使用顺序:
* Lock --》 同步代码块(已经进入了方法体,分配了相应资源) --》 同步方法(在方法体之外)
*
*
* 面试题:如何解决线程安全问题?有几种方式
* @author shkstart
* @create 2019-02-15 下午 3:38
*/
class Window implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock(),把代码放在try-catch-finally中,没有异常所以不用写catch.
//lock()方法后的代码都会被锁住,直到执行unlock()方法释放锁
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
2.5 线程的通信问题
2.5.1 入门案例:wait(),notify(),notifyAll
package com.atguigu.java2;
/**
* 线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
*
* 涉及到的三个方法:
* wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
* notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
* notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
*
* 说明:
* 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
* 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
* 否则,会出现IllegalMonitorStateException异常
* 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
*
* 面试题:sleep() 和 wait()的异同?
* 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
* 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
* 2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
* 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
*
* @author shkstart
* @create 2019-02-15 下午 4:21
*/
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
/*
* 程序执行过程:
* 假如线程1先执行进入方法中,因为没有wait阻塞的方法,此时obj.notify();只是执行下而已,之后打印线程1并进入到阻塞。
* 线程2进来首先唤醒线程1,因为此时同步锁对象是线程2拿着的即便唤醒线程1,线程1也进来,之后打印线程2,线程2进入到阻塞。
* 之后线程1在进来唤醒线程2,此时线程1拿着锁对象线程2唤醒也进不来,打印线程1,线程1进入到阻塞状态。
* 之后线程2进来唤醒线程1.......直到打印完成。
*
* */
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
2.5.2 经典例题:生产者/消费者问题
package com.atguigu.java2;
/**
* 线程通信的应用:经典例题:生产者/消费者问题
*
* 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
* 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
* 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品
* 了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
*
* 分析:
* 1. 是否是多线程问题?是,生产者线程,消费者线程
* 2. 是否有共享数据?是,店员(或产品)
* 3. 如何解决线程的安全问题?同步机制,有三种方法
* 4. 是否涉及线程的通信?是
*
* @author shkstart
* @create 2019-02-15 下午 4:48
*/
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
Consumer c2 = new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}
2.6 多线程创建3:实现Callable接口
JDK5.0新增
package com.atguigu.java2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 创建线程的方式三:实现Callable接口。 --- JDK 5.0新增
*
*
* 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
* 1. call()可以有返回值的。
* 2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
* 3. Callable是支持泛型的
*
* @author shkstart
* @create 2019-02-15 下午 6:01
*/
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
//get方法只是获取call方法的返回值,如果你不感兴趣或者下面执行的代码中也用不到,则不用写这一步。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.7 多线程创建4:使用线程池(开发中最常用)
JDK5.0新增
开发中大多数写线程池是通过框架来实现的,一般不用自己写线程池
package com.atguigu.java2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 创建线程的方式四:使用线程池
*
* 好处:
* 1.提高响应速度(减少了创建新线程的时间)
* 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
* 3.便于线程管理
* corePoolSize:核心池的大小
* maximumPoolSize:最大线程数
* keepAliveTime:线程没有任务时最多保持多长时间后会终止
*
*
* 面试题:创建多线程有几种方式?四种!
* @author shkstart
* @create 2019-02-15 下午 6:30
*/
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池 ExecutorService是接口,Executors.newFixedThreadPool(10)这个工具类是
//为了创建对象,因为接口不能实例化,所以这个工具类的返回值一定是接口的子类对象,多态形式。
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性:
/*直接通过service无法调用:
*因为接口中的属性比较少,而且变量都是常量不允许修改,所以先要设置属性一定是ExecutorService接口的子类中设置。
*而Executors.newFixedThreadPool(10);的返回值恰好是子类对象,因为接收直接用的是多态形式看不到这个子类
*对象是谁,那么这个实现类对象如何看到呢????
* service.getClass():获取这个对象到底是那个类造的。此工具类返回的对象是ThreadPoolExecutor类造的,
* 因为是通过多态父类接收的,想调用子类的属性,可以通过强转实现。即service1调用。
*/
// System.out.println(service.getClass()); //ThreadPoolExecutor
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//...... 查Api即可
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
2.8 四种线程创建方式比较
方式 | 优点 | 缺点 |
---|---|---|
Thread | 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。 | 线程类已经继承了Thread类,所以不能再继承其他父类 |
Runnable | 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。 | 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。 |
Callable | Runnable规定(重写的方法是run(),Callable规定(重写)的方法是call()。Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。Call方法可以抛出异常,run方法不可以。运行Callable任务可以拿到一个Future对象,表示异步计算的结果。 | 存取其他项慢 |
Pool | 线程池可以创建固定大小,这样无需反复创建线程对象,线程是比较耗费资源的资源同时线程不会一直无界的创建下去,拖慢系统 | 编程繁琐,难以理解 |