线程
一、进程
1.1 概述
- 是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空 间,即进程空间或(虚空间)。
1.2 一般而言,进程包含如下三个特点:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
- 并发性:任何进程都可以同其他进行一起并发执行,并不会互相影响;
- 独立性:进程是系统进行资源分配和调度的一个独立单位;
- 结构性:进程由程序,数据和进程控制块三部分组成
现代的操作系统都支持多进程的并发,而且都多数采用效率更高的抢占式多任务策略,但对于cpu来说只能在某一时间点执行一个任务,所以cpu只能在这些任务之中频繁跳动,而我们进行多任务的使用时,并没有感到中断或轮换,这是因为 cpu跳动速率是1/n秒,在多个任务之间跳动。
二、单线程
- 之前我们学习的大部分都是单线程,单线程就是进程只有一个线程,上一个任务完成时,才会进行下一个任务,前面的必须执行完才会执行下一步,如果某一步的代码遇到了阻塞,则程序将会停滞在该处,如果我们使用Eclipse、IDEA等IDE工具的单步调试功能将可以非常清楚地看出这一点 。
三、多线程
- 多线程可以更好地利用cpu的资源
- 多线程:可以指 一个程序(一个进程)在运行时产生了不止一个线程,而产生的多个线程之间互不影响
3.1 概述
- 多线程则是扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务,线程也称为轻量级进程,就像进程在操作系统中地位一样,线程在进程中也是独立的、并发的执行流,当进程被初始化后,主线程就被创建了,对于 Java 程序来说,main线程就是主线程,但我们可以在该进程内创建多条执行路径,称为线程,也就是多线程
- 进程中的每一个线程可以完成一定的任务,并且是独立的,线程可以拥有自己独立的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。由于线程间的通信是在同一个地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快,因此可以通过简单编程实现多线程相互协同来完成进程所要完成的任务。但是也存在安全问题,因为其中一个线程对共享的系统资源的操作都会给其他线程带来影响,由此可知,多线程中的同步是非常重要的问题。
- 线程的执行也是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行。我们说CPU在不同的进程之间轮换,进程又在不同的线程之间轮换,因此线程是CPU执行和调度的最小单元。
- 总之,一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程简单的多,而且多个线程共享同一个进程的虚拟空间,所以,使用多线程来实现并发比使用多进程实现并发的性能要高的多。
- 在实际应用中,多线程是非常有用的,一个Web服务器必须能同时响应多个客户端的请求;一个浏览器必须能同时下载多个图片;一个在线播放器必须能一边下载一边播放…
3.2 注意:
并发性(concurrency)和并行性(parallel)是两个概念,并行是指在同一时刻,有多条指令在多个处理器上同时执行;并发是指在同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
3.3 线程的创建和启动
- Java中Thread 类代表的是线程,所有线程都必须是Thread类或者使其子类的对象,每个线程的作用是完成一定的任务,实际上就是执行一段代码,我们称之为线程执行体。Java使用run方法来封装这段代码,即run方法的方法体就是线程执行体。
四、线程的创建
4.1 继承于Thread 类创建线程类
通过继承 Thread 类来创建并启动多线程:
-
Thread类是描述线程的类,实现多线程类必须继承它
-
实现步骤: + 创建一个 Thread 类的子类 + 在 Thread 类的子类中重写Thread类的run()方法,设置线程任务(线程要做什么) + 创建 Thread 类的子类对象 + 调用 Thread 类中的方法 start 方法,开启新的线程,执行run方法
-
结果是两个线程同时运行:当前线程(主线程main线程)和另一个线程(创建的新线程,执行其run方法)。
-
多次启动一个线程是不合法的。 特别是当线程已经借宿执行后,不能重启线程
package com.bdit.part01.thread;
public class TestMyThread {
public static void main(String[] args) {
MyThread my1 = new MyThread();
my1.start();
MyThread my2 = new MyThread();
my2.start();
for (int i = 10; i >= 1; i--) {
System.out.println(Thread.currentThread().getName() + "线程:" + i);
}
}
}
class MyThread extends Thread{
public void run(){
for (int i = 1; i <= 10; i++) {
System.out.println(super.getName() + "线程:" + i);
}
}
}
4.2 说明
- Thread.currentThread() 方法总是返回当前在执行的线程对象
- getName() 方法是Thread 的实例方法,该方法返回当前线程对象的名字,可以通过setName(String name)方法设置线程名称。
4.3 提示
- JavaSE 的程序至少有一个main主线程,它的方法体也是线程体。
- 多线程之间互不打扰
- 启动线程是 start() 方法,而不是 run() ,调用start() 方法来启动线程,系统会把该run方法当成线程执行体来处理,但是如果之间调用run()系统就会把线程对象当成一个普通方法,而run方法就是一个普通方法
4.4 实现 Runnable 接口
- 步骤
- 定义定义Runnable 接口的实现类,并重写该接口的run() 方法
- 创建Runnable实现类的对象
- 创建 Thread 类的对象,并将Runnable 实现类的对象作为 target。该 Thread类的对象才是真正的线程对象。
- 当JVM调用线程对象 run() 时,如果target不为空,就会调用 target的run()方法
public void run() {
if (target != null) {
target.run();
}
}
- 调用线程对象的 start() 启动线程
package com.bdit.qackub;
public class Qack01 {
public static void main(String[] args) {
MyRunnable myRunnable =new MyRunnable();
new Thread(myRunnable).start();
new Thread(myRunnable).start();
for (int i = 10; i >= 1; i--) {
System.out.println(Thread.currentThread().getName() + "线程:" + i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
五、生命周期
5.1 五种状态
- 一个完整的生命周期拥有以下五种生命周期,新建(New)、就绪(Runnable)、运行(Running)、堵塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程就会在多种状态下来回切换
-
新建状态:当用new创建一个线程时,线程还没有开始,此时的线程就处于新建状态,仅仅由Java分配了内存,并初始化了实例变量的值,但此时线程对象并没有任何线程的动态特征,程序也不会执行他的线程体 run()
-
就绪状态:一个线程创建后,并不会自动执行,如诺想要执行则需调用 start()方法,这时的线程就处于就绪状态了,而在这个状态的线程,并没有运行但已经有了运行的条件。
处于此状态的线程并不一定会运行 run() 方法,因为线程必须要和其它线程竞争CPU的资源,只有获得CPU的资源才会执行这个线程,同理,在多线程中,会有多个线程进入就绪状态也就会有多个线程一起竞争一个CPU资源。
-
运行状态:前提处于就绪状态的线程,处于就绪状态的线程活得了CPU的资源,就会进入运行状态,真正执行run() 方法的线程代码。
Java是抢占式资源,系统会给每一个可执行的线程一个时间段来进行处理任务,但当时间用完,系统就会剥夺此资源,使其回到就绪状态等待下次,而系统在选择线程时,会适当考虑优先级(使用优先级的系统很少,几乎可以忽略)
-
阻塞状态:线程运行过程中,可能会进入阻塞状态:
- 线程通过调用 sleep() 进入睡眠状态
- 线程调用一个在阻塞式IO的操作,即该操作在输入输出操作完成之前不会返回到他的调用者;
- 运行的线程在获取对象的同步锁时,诺该同步锁被别的线程占用,则JVM会把该线程放入锁池中
- 在线程的执行过程中,遇到了其它线程对象的加塞(join)、同步监视器调用了wait(),让它等待某个通知(notify)。
-
死亡状态:结束后的线程就是死亡状态
- run() 的正常退出
- 异常未捕获或者有错误而退出程序
- 可以调用线程的isAlive()方法来判断线程是否死亡,当线程处于(就绪、运行、阻塞、)这三种状态时,则会返回true、当线程处于(新建、死亡)这两种状态时,返回false
- 注意:
- 程序只能对 新建状态的线程调用 start() 方法,对死亡、以启动的线程调用则会报错IllegalThreadStateException异常。
六、Thread类的方法
6.1 创建线程对象相关
- 构造器:
- Thread():创建新的Thread对象
- Thread(String threadname):创建线程,并为其指定名字
- Thread(Runnable target):指定创建线程的目标对象,实现了 Runnable接口中的 run方法
- Thread(Runnable target,String name):创建新的Thread对象
- 编写线程体和启动线程:
- public void run():子类必须重写run()以编写线程体
- public void start():启动线程
6.2 获取和设置线程信息
- public static Thread currentThread():这是一个静态方法,总是返回当前在执行的线程对象
- public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
- public final String getName():getName()方法是Thread的实例方法,该方法返回当前线程对象的名字,可以通过
- public final void setName(String name):设置该线程名称。除了主线程main之外,其他线程可以在创建时指定线程名称或通过setName(String name)方法设置线程名称,否则依次为Thread-0,Thread-1…等。
- public final int getPriority() :返回线程优先值
- public final void setPriority(int newPriority) :改变线程的优先级(几乎没用)
每个线程都会有优先级,默认是与其父类优先级一致,但是Thread可以通过方法设置
Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
七、控制线程
7.1 线程睡眠:sleep
在运行时,我们想要使正在执行的线程暂停一段时间,可以使用 sleep方法,它会使当前线程进入堵塞状态。
sleep方法会产生异常,直接try-catch 即可
- public static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
- public static void sleep(long millis,int nanos):在指定的毫秒加纳秒内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
package com.bdit.exercise;
public class Exercise01 {
public static void main(String[] args) {
Thread t = new Thread(new MySleep());
t.start();
}
}
class MySleep extends Thread{
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
7.2 线程让步:yield
- yield() 也是一个静态方法,它可以让正在执行的线程暂停,但它不会阻塞该线程,他只是将该线程转入就绪状态
- yield() 只是让当前线程暂停一下,使系统重新调用,使其它优先度高的可以获得执行机会,但也不确定不会调用自己,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
package com.bdit.exercise;
public class Exercise01 {
public static void main(String[] args) {
Thread t01 = new Thread(new Myyield(),"one");
t01.setPriority(Thread.MIN_PRIORITY);//线程优先度最低
Thread t02 = new Thread(new Myyield(),"two");
t02.setPriority(Thread.MAX_PRIORITY);//线程优先度最高
t01.start();
t02.start();
}
}
class Myyield extends Thread{
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
Thread.yield();
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
7.3 线程加塞:join
- 当在某个线程的线程体中调用了另一个线程的 join() 方法,当前线程将会被堵塞,知道join进来的线程执行完它才继续。
join方法有异常,直接 try-catch 即可
- void join() :等待该线程终止。
- void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
- void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
package com.bdit.exercise;
public class Exercise01 {
public static void main(String[] args) {
Thread t =new Thread(new Myjoin());
t.start();
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
if (i==5){
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Myjoin extends Thread{
@Override
public void run() {
for (int i = 1; i <=5 ; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
7.4 守护线程
- 有一种线程,实在后台运行的它的任务是为其它线程提供服务的,这种线程被称为”守护线程“,JVM的垃圾回收线程就是典型的守护线程
- 特点:在所有非守护线程死亡后,守护线程自动死亡
- 调用 setDeamon(true) 方法可以将指定线程变成守护线程,必须在线程启动之前设置,否则会出现 IllegalThreadStateException异常。
- 调用 isDeamon() 可以判断线程是否是守护线程
package com.bdit.exercise;
public class Exercise01 {
public static void main(String[] args) {
MyDeamon myDeamon = new MyDeamon();
myDeamon.setDaemon(true);
myDeamon.start();
for (int i = 5; i <= 10; i++) {
System.out.println("-->"+i);
}
System.out.println("判断线程t01是否是守护线程:"+myDeamon.isDaemon());
}
}
class MyDeamon extends Thread{
@Override
public void run() {
while (true){
System.out.println("MyDeamon");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
7.5 停止线程
- 正常的线程执行完或者遇到异常就会停止,但是我们想由另一个线程检测到某个情况来停止线程,则: 一是通过stop() 来停止,但是这个方法已被不建议使用,二是通过标识来停止
案例:编写龟兔赛跑多线程程序,设赛跑长度为30米
兔子的速度是10米每秒,兔子每跑完10米休眠的时间10秒
乌龟的速度是1米每秒,乌龟每跑完10米的休眠时间是1秒
要求:只要兔子和乌龟中有人到达终点,就宣布比赛结束,没到达终点的也停下来。
package com.bdit.part02;
public class Player extends Thread{
private String name;//运动员名字
private long runTime;//每米需要时间,单位毫秒
private long restTime;//每10米的休息时间,单位毫秒
private long distance;//全程距离,单位米
private long time;//跑完全程的总时间
```
private boolean flag = true;//用于标记是否继续跑,即结束线程的标记
private volatile boolean ended = false;//用于标记是否到达终点
public Player(String name, long distance, long runTime, long restTime) {
super();
this.name = name;
this.distance = distance;
this.runTime = runTime;
this.restTime = restTime;
}
@Override
public void run() {
long sum = 0;
long start = System.currentTimeMillis();
while (sum < distance && flag) {
System.out.println(name + "正在跑...");
try {
Thread.sleep(runTime);// 每米距离,该运动员需要的时间
} catch (InterruptedException e) {
return ;
}
sum++;
try {
if (sum % 10 == 0 && sum < distance && flag) {
// 每10米休息一下
System.out.println(name+"已经跑了"+sum+"米正在休息....");
Thread.sleep(restTime);
}
} catch (InterruptedException e) {
return ;
}
}
long end = System.currentTimeMillis();
time = end - start;
ended = sum == distance ? true : false;
System.out.println(name+"跑了"+sum+"米,共用时"+time/1000.0+"秒");
}
public long getTime() {
return time;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean isEnded() {
return ended;
}
```
}
package com.bdit.part02;
public class TestStop {
public static void main(String[] args) {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
Player rabbit = new Player("兔子", 30, 100, 10000);
Player turtoise = new Player("乌龟", 30, 1000, 1000);
```
rabbit.start();
turtoise.start();
while(true){
if(rabbit.isEnded() || turtoise.isEnded()){
rabbit.setFlag(false);
turtoise.setFlag(false);
//只要又人跑完,就结束比赛,并公布结果
break;
}
}
System.out.println("比赛结束");
if(rabbit.isEnded() && turtoise.isEnded()){
System.out.println(rabbit.getTime()<turtoise.getTime()?"兔子赢":"乌龟赢");
}else if(rabbit.isEnded()){
System.out.println("兔子赢");
}else if(turtoise.isEnded()){
System.out.println("乌龟赢");
}
}
```
}
提示:
volatile的作用是确保不会因编译器的优化而省略某些指令,volatile的变量是说这变量可能会被意想不到地改变,每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份,这样,编译器就不会去假设这个变量的值了。
八、线程安全和线程同步
8.1 线程安全
- 当多个线程共同访问一组数据时,就会发生线程安全问题。
8.2 线程安全问题
关于线程安全问题,有一个经典的问题:卖票问题。卖票的基本流程很简单:看是否还有票,如果有就可以卖。
现在开启多个窗口同时卖票:
package com.bdit.exercise;
public class Exercise01 {
public static void main(String[] args) {
MySetick mySetick = new MySetick();
Thread t01 = new Thread(mySetick,"窗口一");
Thread t02 = new Thread(mySetick,"窗口二");
Thread t03 = new Thread(mySetick,"窗口三");
t01.start();
t02.start();
t03.start();
}
}
class MySetick extends Thread{
private int tick = 100;
@Override
public void run() {
while (true){
if (tick>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖" + tick + "票");
tick--;
}else {
break;
}
}
}
}
- 注意:这样会出现重复票和不存在票,这样就是线程不安全,所以要解决
8.3 同步代码块
【线程同步也就是线程安全】
【多线程只要不牵扯操作成员变量的问题,就不会引发线程安全问题】
- 解决线程安全
- 对多条操作共享数据的语句,只能然一个线程都执行完,在执行的过程中,其他线程不能参于
- 可以使用同步监视器
- 第一种:
- synchronized(同步监视器对象){ }
上面代码的含义,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得对同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器对象的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。
Java程序运行使用任何对象来作为同步监视器对象,只要保证共享资源的这几个线程,锁的是上面代码的含义,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得对同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器对象的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。
Java程序运行使用任何对象来作为同步监视器对象,只要保证共享资源的这几个线程,锁的是同一个同步监视器对象即可。同步监视器对象即可。
如果线程是继承Thread类实现的,那么把同步监视器对象换成this,那么就没有起到作用,仍然会发生线程安全问题。因为两个线程的this对象是不同的。
但是如果线程是实现Runnable接口实现的,那么如果两个线程共用同一个Runnable接口实现类对象作为target的话,就可以把同步监视器对象换成this。
package com.bdit.vifition;
public class Vifi_09_01 {
public static void main(String[] args) {
MySetickets mySetickets = new MySetickets();
Thread t01 = new Thread(mySetickets,"窗口一");
Thread t02 = new Thread(mySetickets,"窗口二");
Thread t03 = new Thread(mySetickets,"窗口三");
t01.start();
t02.start();
t03.start();
}
}
class MySetickets implements Runnable{
private int ticket =100;
@Override
public void run() {
while (true){
synchronized (this){
if (ticket>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖"+ticket+"票");
ticket--;
}else {
break;
}
}
}
}
}
8.4 继承Thread 类和实现Runnable 接口的两种方式的区别
通过继承Thread类或实现Runnable接口都可以实现多线程,但是两种方式存在一定的差别:
采用继承Thread的方式:
优势
(1)直接线程对象启动,比较简单。
(2)如果在线程体中要访问当前线程,直接this即可。
劣势:
(1)Java有单继承线程,继承了Thread类就不能再继承其他类了。
(2)如果多个线程要共享数据,就比较麻烦,使用static的变量共享,范围又太大。
采用Runnable的方式:
优势
(1)避免单继承;
(2)可以多个线程共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
劣势:
(1)启动线程需要再new Thread的对象;
(2)在线程体中要访问当前对象,需要用Thread.currentThread()先获取当前线程对象。
8.5 同步方法
与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显式指定同步监视器,静态方法的同步监视器对象是当前类的Class对象,非静态方法的同步监视器对象是调用当前方法的this对象。
package com.bdit.vifition;
public class Vifi_09_02 {
public static void main(String[] args) {
MySeticketsa mySeticketsa = new MySeticketsa();
Thread t01 = new Thread(mySeticketsa,"窗口一");
Thread t02 = new Thread(mySeticketsa,"窗口二");
Thread t03 = new Thread(mySeticketsa,"窗口三");
t01.start();
t02.start();
t03.start();
}
}
class MySeticketsa implements Runnable{
private int ticket =100;
@Override
public void run() {
tick();
}
public synchronized void tick(){
while (true) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖" + ticket + "票");
ticket--;
}else {
break;
}
}
}
}
package com.bdit.vifition;
public class Vifi_09_03 {
public static void main(String[] args) {
MySeck mySeck = new MySeck();
Thread t01 = new Thread(mySeck,"窗口一");
Thread t02 = new Thread(mySeck,"窗口二");
Thread t03 = new Thread(mySeck,"窗口三");
t01.start();
t02.start();
t03.start();
}
}
class MySeck implements Runnable{
private static int ticket =100;
@Override
public void run() {
tick();
}
public static /*synchronized*/ void tick(){
synchronized (MySeck.class){
while (true) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖" + ticket + "票");
ticket--;
}else {
break;
}
}
}
}
}
九、 释放同步监视器的锁定
9.1 释放锁的操作
-
当前线程的同步方法、同步代码块执行结束
-
当前线程在同步代码块、同步方法中遇到了break、return 终止了该代码块、该方法继续执行
-
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
-
当前线程在同步代码块、同步方法中执行了锁对象的
wait()方法,当前线程被挂起,并释放锁
9.2 不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()这样的过时来控制线程
9.3 死锁
不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
【如何避免线程出现死锁?加大锁的粒度】
package com.bdit.text;
public class Text06 {
public static void main(String[] args) {
Object obj01 = new Object();
Object obj02 = new Object();
SiAsuo siAsuo = new SiAsuo(obj01,obj02);
SiBsuo siBsuo = new SiBsuo(siAsuo);
new Thread(siAsuo).start();
new Thread(siBsuo).start();
}
}
class SiAsuo implements Runnable{
Object obj01 = new Object();
Object obj02 = new Object();
public SiAsuo(Object obj01, Object obj02){
this.obj01 = obj01;
this.obj02 = obj02;
}
@Override
public void run() {
synchronized (obj01){
System.out.println("AAAA");
synchronized (obj02){
System.out.println("BBBBB");
}
}
}
}
class SiBsuo implements Runnable{
private SiAsuo sa;
public SiBsuo(SiAsuo sa){
this.sa = sa;
}
@Override
public void run() {
synchronized (sa.obj02){
System.out.println("BBBBB");
synchronized (sa.obj01){
System.out.println("AAAAA");
}
}
}
}
十、线程通信
- 生产者与消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。
该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
生产者与消费者问题中其实隐含了两个问题:
- 线程安全问题:因为生产者与消费者共享数据缓冲区,不过这个问题可以使用同步解决。
- 线程的协调工作问题:
要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知(notify)正在等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让消费者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题。
10.1 案例:快餐店的厨师、服务员、取餐台
- 一个生产者、一个消费者
案例:有加餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务器从这个工作台取出快餐给顾客。现在有一个厨师和一个服务员。
package com.bdit.questop;
//一个生产者一个消费者
public class Quest05 {
public static void main(String[] args) {
WorkTai wk = new WorkTai();
Cook ck = new Cook(wk);
Wrick rk = new Wrick(wk);
ck.start();
rk.start();
}
}
class WorkTai extends Thread{
private static final int max_num = 10;
int num = 0;
@Override
public void run() {}
public synchronized void make(){
if (num>max_num){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println("厨师制作"+num);
this.notify();
}
public synchronized void shangcai(){
if(num<0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("服务员送走"+num);
num--;
this.notify();
}
}
class Cook extends Thread{
private WorkTai wk;
public Cook(WorkTai wk){
this.wk = wk;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
wk.make();
}
}
}
class Wrick extends Thread{
private WorkTai wk;
public Wrick(WorkTai wk){
this.wk = wk;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
wk.shangcai();
}
}
}
10.2 多个生产者与多个消费者
案例:有加餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务器从这个工作台取出快餐给顾客。现在有多个厨师和多个服务员。
package com.bdit.questop;
public class Quest06 {
public static void main(String[] args) {
WorkTaia wka = new WorkTaia();
Cooka cka1 = new Cooka(wka);
Wricka rka1 = new Wricka(wka);
Cooka cka2 = new Cooka(wka);
Wricka rka2 = new Wricka(wka);
Cooka cka3 = new Cooka(wka);
Wricka rka3 = new Wricka(wka);
cka1.start();
rka1.start();
cka2.start();
rka2.start();
cka3.start();
rka3.start();
}
}
class WorkTaia extends Thread{
private static final int max_num = 10;
int num = 0;
public synchronized void make(){
while (num>=max_num){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName()+"厨师制作"+num);
this.notify();
}
public synchronized void shangcai(){
while(num<=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"服务员送走"+num);
num--;
this.notify();
}
}
class Cooka extends Thread{
private WorkTaia wka;
public Cooka(WorkTaia wka){
this.wka = wka;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
wka.make();
}
}
}
class Wricka extends Thread{
private WorkTaia wka;
public Wricka(WorkTaia wka){
this.wka = wka;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
wka.shangcai();
}
}
}
十一 、单例(单态)设计模式
-
单例设计模式,是软件开发中最常用的设计模式之一,它是指某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。例如:代表JVM运行环境的Runtime类。
通常有饿汉式和懒汉式两种
11.1 饿汉式
饿汉式就是在类的初始化时,直接创建对象。
优势:Java的类加载和初始化机制可以保证线程安全,所以这类形式的单例设计模式不存在线程安全问题
【java加载类是通过一个 ClassLoader类加载器来完成的】
劣势:不管你暂时是否需要改实例对象,都会创建,使得过分浪费支援,类的初始化时间加长
- 直接实例化饿汉式
package com.bdit.test;
//饿汉式
public class Text07 {
public static void main(String[] args) {
}
}
class Hungry{
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
- 枚举式
package com.bdit.part05;
public enum Singleton2 {
INSTANCE}
- 静态代码块饿汉式
package com.bdit.part05;
import java.io.IOException;
import java.util.Properties;
public class Singleton3 {
public static final Singleton3 INSTANCE;
private String info;
static{
try {
Properties pro = new Properties();
pro.load(Singleton3.class.getClassLoader().getResourceAsStream("single.properties"));
INSTANCE = new Singleton3(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Singleton3(String info){
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
11.2 懒汉式
所谓懒汉式,即延迟创建对象,知道用户来获取这个对象时,在创建
优势:不用不创建,用时在创建
劣势:线程安全问题
package com.bdit.test;
//懒汉式
public class Lanhan {
private Lanhan(){
}
private volatile static Lanhan Lanhan;
public static Lanhan getInstance(){
if (Lanhan == null){
synchronized (Lanhan.class){
if (Lanhan == null){
Lanhan = new Lanhan();
}
}
}
return Lanhan;
}
public static void main(String[] args) {
for (int i = 0; i <= 5; i++) {
System.out.println(i);
}
}
}
十二、线程池
12.1 线程池思想概述
- 线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
12.2 线程池的工作机制
- 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
- 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
12.3 使用线程池的原因:
多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。
12.4 线程池应用
java里面线程池的父接口是java.util.concurrent. Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是java.util.concurrent. ExecutorService
要配置一个线程池是比较复杂的,尤其是对线程池的原理不很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池,官方也建议使用Executors工厂类来创建线程池对象。
Executors类由个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,创建的是有界限的线程池,也就是线程池中的个数可以指定最大数量。
获取到了一个线程池对象,那么怎么使用呢?
public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。
使用线程池的步骤:
-
创建线程池对象
-
创建Runnable接口子类对象
-
提交Runnable接口子类对象
-
关闭线程池(一般不做)
package com.bdit.questop;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Quest09 {
public static void main(String[] args) {
ExecutorService exser = Executors.newFixedThreadPool(2);
Exccc exccc = new Exccc();
exser.submit(exccc);
exser.submit(exccc);
exser.submit(exccc);
exser.shutdown();
}
}
class Exccc implements Runnable{
@Override
public void run() {
System.out.println("线程");
}
}
溃。这时,线程池就是最好的选择了。
12.4 线程池应用
java里面线程池的父接口是java.util.concurrent. Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是java.util.concurrent. ExecutorService
要配置一个线程池是比较复杂的,尤其是对线程池的原理不很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池,官方也建议使用Executors工厂类来创建线程池对象。
Executors类由个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,创建的是有界限的线程池,也就是线程池中的个数可以指定最大数量。
获取到了一个线程池对象,那么怎么使用呢?
public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。
使用线程池的步骤:
-
创建线程池对象
-
创建Runnable接口子类对象
-
提交Runnable接口子类对象
-
关闭线程池(一般不做)
package com.bdit.questop;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Quest09 {
public static void main(String[] args) {
ExecutorService exser = Executors.newFixedThreadPool(2);
Exccc exccc = new Exccc();
exser.submit(exccc);
exser.submit(exccc);
exser.submit(exccc);
exser.shutdown();
}
}
class Exccc implements Runnable{
@Override
public void run() {
System.out.println("线程");
}
}