10.线程
10.1.背景知识
- 进程
- 官方点的理解:计算机程序在某个数据集合上的运行活动,进程是操作系统进程资源分配与调度的基本单位。即:正在运行的程序(软件)。
- 每个进程都被操作系统分配了自己独立的内存空间与系统资源,进程之间互不干扰。
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程与线程调度
- 线程依赖于进程而存在,一个进程有多个子任务,而线程就是进程中的一个子任务,一个执行单元,一个顺序控制流。线程是CPU进行资源分配与调度的进本单位。JVM是多线程的。
- 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,线程之间共享进程的数据。这个应用程序也可以称之为多线程程序。
- 线程调度:
- 协同式线程调度(Cooperative Thread-Scheduling):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,通知系统切换到另外一个线程上去。
- 其最大的好处是实现简单,坏处是线程执行时间不可控
- 抢占式线程调度(Preemptive Thread-Scheduling):优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。
- 使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。
- 其最大的好处是,线程的执行时间是可控的
- 设置线程的优先级: 我们在java语言中设置的线程优先级,它仅仅只能被看做是一种”建议”(对操作系统的建议),实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级),线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点
- 获取线程优先级:public final int getPriority()
- 设置线程优先级:public final void setPriority(int newPriority)
- 优先级的范围:1-10,默认是5
- MIN_PRIORITY = 1
- NORM_PRIORITY = 5
- MAX_PRIORITY = 10
- 抢占式调度详解:大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。这些程序是在同时运行,实际上CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。Java语言采用的是抢占式调度。
- 协同式线程调度(Cooperative Thread-Scheduling):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 串行,并行,并发。
- 串行:一个任务接一个任务的执行
- 并发:同一时间段内,多个任务同时执行
- 并行:同一时刻,多个任务同时执行
- 并行是一种理想状态下的并发。
10.2.Thread类
- java.lang.Thread 类,API中该类中定义了有关线程的一些方法
- 构造方法:
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
- 常用方法:
-
public String getName() :获取当前线程名称。
-
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
-
public void run() :此线程要执行的任务在此处定义代码。
-
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
-
public static Thread currentThread() :返回对当前正在执行的线程对象的引用
- Thread.sleep():参数是毫秒
- 等效的方法 TimeUnit.SECONDS.sleep(10):休眠10秒,单位更加具体
-
public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出
-
public static void yield():暂停当前正在执行的线程对象,并执行其他线程
-
public void interrupt():方法只是改变中断状态而已,它不会中断一个正在运行的线程
-
public final void join(): 等待该线程终止,Join这行代码在哪个线程上运行,就是哪个线程在等待,等待的是使用join方法的这个线程对象执行完。
实际上,join方法使得等待的线程阻塞,阻塞的是join这行代码在执行的那个线程上的线程。
-
//1.获取线程名称
public class Demo {
public static void main(String[] args) {
// 获取main线程名
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
// 创建对象
MyThread t1 = new MyThread();
//Thread(String name) 利用构造函数给线程设置名字
// 分配新的 Thread 对象。
// 给线程设置名字
t1.setName("吴彦祖");
// 通过getName方法得到的默认线程名 Thread-0
String name = t1.getName();
MyThread t2 = new MyThread("彭于晏");
String name1 = t2.getName();
System.out.println(name1);
//System.out.println(name1);
//System.out.println(name);
//启动线程
t1.start();
//t2.start();
}
}
// 继承Thread
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
public MyThread() {
}
@Override
public void run() {
// 在run方法中也能获取到线程名
System.out.println(Thread.currentThread().getName() + ":hello thread");
}
}
//2.守护线程
public class ThreadDaemonDemo {
public static void main(String[] args) throws InterruptedException {
// 创建对象
ThreadDaemon threadDaemon = new ThreadDaemon();
threadDaemon.setName("PDD");
// 标记成守护线程
threadDaemon.setDaemon(true);
// 启动线程
threadDaemon.start();
// 休眠3s
Thread.sleep(3000);
//System.out.println("123");
}
}
class ThreadDaemon extends Thread {
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "-----" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//3.线程合并
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("main start");
// 创建对象
ThreadJoin threadJoin = new ThreadJoin();
threadJoin.setName("正方形打野");
// 开启线程
threadJoin.start();
threadJoin.join();
System.out.println("main end");
}
}
class ThreadJoin extends Thread {
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "------" + i);
}
}
}
//4.线程休眠
public class ThreadSleepDemo {
public static void main(String[] args) {
// 创建子类对象
ThreadSleep t = new ThreadSleep();
// 启动线程
t.start();
}
}
class ThreadSleep extends Thread {
@Override
public void run() {
System.out.println("sleep before");
try {
// 当休眠时间到,就会自己醒来。
Thread.sleep(2000);
// TimeUnit.MILLISECONDS.sleep() 可以按毫秒,秒,分钟,小时,天
// TimeUnit.SECONDS.sleep(2);
// TimeUnit.HOURS.sleep(1);
// TimeUnit.DAYS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sleep after");
}
}
//5.线程终止
public class ThreadStop {
public static void main(String[] args) throws InterruptedException {
// 创建对象
ThreadInterrupt threadInterrupt = new ThreadInterrupt();
// 启动线程
threadInterrupt.start();
Thread.sleep(3000);
// 泼了一盆冷水,叫醒
// 实际上是根据java当中的异常处理机制
//threadInterrupt.interrupt();
threadInterrupt.stop();
}
}
class ThreadInterrupt extends Thread {
// 重写run方法
@Override
public void run() {
System.out.println("run start");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// stop方法具有固有的不安全性
// 这里就不会再执行了
System.out.println("run end");
}
}
//6.线程礼让
public class ThreadYieldDemo {
public static void main(String[] args) {
// 创建对象
ThreadYield threadYield = new ThreadYield();
ThreadYield threadYield2 = new ThreadYield();
threadYield.setName("55开");
threadYield2.setName("卢本伟");
// 启动2个线程
threadYield.start();
threadYield2.start();
}
}
class ThreadYield extends Thread {
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"------"+i);
yield();
// 暂停当前正在执行的线程对象,
// 并执行其他线程。
// 虽然放弃了CPU的执行权,但是我们java当中采用的是抢占式的调度方式
// 所以下一次不一定是谁抢到
}
}
}
10.3.实现多线程
- 创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式
10.3.1.方法一
- Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
public class MyThread extends Thread{
/*
* 利用继承中的特点
* 将线程名称传递 进行设置
*/
public MyThread(String name){
super(name);
}
/*
* 重写run方法
* 定义线程要执行的代码
*/
public void run(){
for (int i = 0; i < 20; i++) {
//getName()方法 来自父亲
System.out.println(getName()+i);
}
}
}
public class Demo {
public static void main(String[] args) {
System.out.println("这里是main线程");
MyThread mt = new MyThread("小强");
mt.start();//开启了一个新的线程
for (int i = 0; i < 20; i++) {
System.out.println("旺财:"+i);
}
}
}
//对于这种方式实现的多线程,如果存在变量不一致的问题,可以采用static关键字来解决。
- 注意事项:
- 一个Thread子类对象代表一个线程
- 只有Thread run()方法中的代码,才会执行在子线程中,保证运行其中的是我们想要在子线程中运行的代码(run方法封装的是被线程执行的任务)
- 一个方法,被哪个线程中的代码调用,被调用的方法,就运行在,调用它的线程中
- 启动线程,必须使用start()方法来启动,才能是Thread中的run方法运行在子线程中。调用run方法执行Thread的run方法代码,这仅仅只是普通的方法调用
- 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次,如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象
10.3.2.方式二
- 实现 java.lang.Runnable 也是非常常见的一种,只需要重写run方法即可。
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
- Thread类实际上也是实现了Runnable接口的类。
- 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
- 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。
10.3.4.匿名内部类创建线程
public class NoNameInnerClassThread {
public static void main(String[] args) {
//new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:"+i);
// }
// }
//}; //‐‐‐这个整体 相当于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
10.4.Thread和Runnable的区别
- 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
- 实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免Java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable的类的线程,不能直接放入继承Thread的类。
注:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程
10.5.线程安全
10.5.1.线程同步
- 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
- 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了线程同步机制(synchronized)来解决。
- 三种方法:
- 同步代码块。
- 同步方法。
- 锁机制。
10.5.2.同步代码块
- synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){
//需要同步操作的代码
}
public class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
synchronized (lock) {
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket‐‐);
}
}
}
}
}
public class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
同步锁:
- 锁对象可以是任意的Java对象。
- 对于非static方法,同步锁对象就是this。
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
10.5.3.同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){
//可能会产生线程安全问题的代码
}
public class Ticket implements Runnable{
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
sellTicket();
}
}
/*
* 锁对象:是谁调用这个方法,就是谁
* 隐含锁对象就是 this
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket‐‐);
}
}
}
10.5.4.Lock锁
- java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
- Lock锁也称同步锁,加锁与释放锁方法化了:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();
}
}
}
10.6.线程状态
- java.lang.Thread.State 这个枚举中给出了六种线程状态
线程状态 | 导致状态发生条件 |
---|---|
New(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
TimedWaiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
10.6.1.Timed Waiting计时等待
- 一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态
- 当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)。
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("‐‐‐‐‐‐‐" + i);
}
System.out.print(i);
try {
Thread.sleep(1000);
System.out.print(" 线程睡眠1秒!\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new MyThread().start();
}
}
- 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
- 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
- sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
- sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
10.6.2.BLOCKED(锁阻塞)
- 一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
10.6.3.Waiting(无限等待)
- 一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
public class WaitingTest {
public static Object obj = new Object();
public static void main(String[] args) {
// 演示waiting
new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (obj){
try {
System.out.println( Thread.currentThread().getName() +"=== 象,调用wait方法,进入waiting状态,释放锁对象");
obj.wait(); //无限等待
//obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了");
}
}
}
},"等待线程").start();
new Thread(new Runnable() {
@Override
public void run() {
// while (true){ //每隔3秒 唤醒一次
try {
System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象");
obj.notify();
}
}
// }
},"唤醒线程").start();
}
}
- 通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。
- 其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
- 当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
注:
我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。
10.7.死锁
- 死锁:线程一占用A资源,等待B资源,线程二占用B资源,等待A资源,这种互锁现象称为死锁
个人理解:死锁如果用通俗易懂的话来解释,其实就像我们生活中,和迎面而来的人相遇,给对面的人让路,对面的人也给我们让路,然而两个人让路的方向是同一边,这样就是死锁。
- 解决方法:
- 更改加锁顺序
- 新加一把锁,把这个操作变成原子操作
public class Demo {
public static void main(String[] args) {
DieLock dieLock1 = new DieLock(true);
DieLock dieLock2 = new DieLock(false);
new Thread(dieLock1).start();
new Thread(dieLock2).start();
}
}
// 描述锁对象
class MyLock {
public static final Object objA = new Object();
public static final Object objB = new Object();
public static final Object objAB = new Object();
}
class DieLock implements Runnable {
boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if A");
// 发生了线程切换
synchronized (MyLock.objB) {
System.out.println("if B");
}
}
}
}
else {
synchronized (MyLock.objB) {
System.out.println("else B");
// 打印了else B
synchronized (MyLock.objA) {
System.out.println("else A");
}
}
}
//解决方法:
//1.新加一把锁
if (flag) {
synchronized (MyLock.objAB) {
synchronized (MyLock.objA) {
System.out.println("if A");
// 发生了线程切换
synchronized (MyLock.objB) {
System.out.println("if B");
}
}
}else {
synchronized (MyLock.objAB) {
synchronized (MyLock.objB) {
System.out.println("else B");
// 打印了else B
synchronized (MyLock.objA) {
System.out.println("else A");
}
}
}
}
//2.更改加锁顺序,就是把原来程序的其中一个分支调换顺序
}
}
10.8.线程通信
- 以生产者,消费者,包子铺为例子,说明线程通信
public class Box {
// 包子
Food food;
// 生产包子 只有生产者线程调用setFood方法
public synchronized void setFood(Food newFood) throws InterruptedException {
//先判断蒸笼是否为空
//如果蒸笼为空,没有包子
if (food == null) {
//做包子并放入蒸笼,
food = newFood;
System.out.println(Thread.currentThread().getName() + "做了:" + food);
Thread.sleep(3000);
// 通知消费者来吃
notify();
//notifyAll();
} else {
//如果蒸笼非空
//说明蒸笼里有包子,
// 阻止自己,不在生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("生产者 wait 后面的代码");
}
}
// 吃包子 只有消费者线程调用eatFood方法
public synchronized void eatFood() throws InterruptedException {
// 先判断蒸笼状态
if (food == null) {
//如果蒸笼为空,没有包子
//阻止自己
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("消费者 wait 后面的代码");
} else {
//如果蒸笼非空 有包子
//吃包子
System.out.println(Thread.currentThread().getName() + "吃了:" + food);
Thread.sleep(3000);
food = null;
//通知生产者去再生产包子
notify();
//notifyAll();
}
}
}
// 该类描述包子
class Food {
String name;
int price;
public Food(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Food{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
public class ConsumerTask implements Runnable {
// 蒸笼
Box box;
public ConsumerTask(Box box) {
this.box = box;
}
@Override
public void run() {
while (true) {
try {
box.eatFood();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProducerTask implements Runnable {
Box box;
Food[] foods = {new Food("庆丰包子", 8),
new Food("上海生煎包", 2), new Food("广式叉烧包", 10)};
Random random;
public ProducerTask(Box box) {
this.box = box;
random = new Random();
}
@Override
public void run() {
while (true) {
// 只生产包子
int index = random.nextInt(foods.length);
try {
box.setFood(foods[index]);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Box box = new Box();
ProducerTask producerTask = new ProducerTask(box);
ConsumerTask consumerTask = new ConsumerTask(box);
//创建2个线程并启动
// 生产者线程
Thread t1 = new Thread(producerTask);
// 消费者线程
Thread t2 = new Thread(consumerTask);
Thread t3 = new Thread(producerTask);
// 消费者线程
Thread t4 = new Thread(consumerTask);
t1.setName("生产者1");
t2.setName("消费者1");
t3.setName("生产者2");
t4.setName("消费者2");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
- 解释:
- 对于notify方法,两个生产者,两个消费者,当生产者生产完毕后,通知的却不一定是消费者,notify方法是随机的,如果另一个生产者抢到了执行权,进入if判断语句,发现已经有包子了,那么程序就会执行else语句,进入等待。
- 对于notifyAll方法,当生产者生产完毕后,唤醒所有线程,三个线程需要争夺执行权限,此时如果另一个生产者抢到了执行权,会进入阻塞状态,剩余两个消费者再次争夺,当某个消费者消费完成后,执行notifyAll方法,唤醒其余线程,进入阻塞状态的生产者会立即执行完wait后面的代码,并立即加入本轮争夺,也就是说,此次消费者消费完成后,依旧是有两个生产者争夺执行权。
- 某个线程想要执行,需要两个条件同时满足,第一,获取到锁对象,第二,抢到了cpu执行权,即其他线程调用了notify方法或notifyAll方法,本线程争夺到了cpu执行权。但是需要注意的是,两个条件必须同时满足,假设这样一种情形,某个程序只有两个线程,一个线程正在运行时,调用了notify方法或notifyAll方法,但是自身却没有执行wait方法释放锁对象,那么另一个线程就会进入阻塞状态,直到正在运行的线程释放了锁对象。
10.9.线程池
10.9.1.概念
- 一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
- 优点:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
10.9.1.线程池的创建
- Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象
- 不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池 Executors.newFixedThreadPool(int n); //创建固定容量大小为n的缓冲池
- 一般需要根据任务的类型来配置线程池大小:
- 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 N(CPU) + 1
- 如果是IO密集型任务,参考值可以设置为2*N(CPU)
- 不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
- 线程池的创建:JDK5提供了Executors来产生线程池,有如下方法
- public ExecutorService newCachedThreadPool()
- 会根据需要创建新线程,也可以自动删除,60s处于空闲状态的线程
- 线程数量可变,立马执行提交的异步任务(异步任务:在子线程中执行的任务)
- public ExecutorService newFixedThreadPool(int nThreads)
- 线程数量固定
- 维护一个无界队列(暂存已提交的来不及执行的任务)
- 按照任务的提交顺序,将任务执行完毕
- public ExecutorService newSingleThreadExecutor()
- 单个线程
- 维护了一个无界队列(暂存已提交的来不及执行的任务)
- 按照任务的提交顺序,将任务执行完毕
- public ExecutorService newCachedThreadPool()
10.9.2.线程池的使用
- 定义了一个使用线程池对象的方法如下:
- public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
- 使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
- ExecutorService(接口):Future submit(Callable task)
- 代表有返回值的异步任务
- Future对象代表异步任务的计算结果 可通过get方法获取
- public Future<?> submit(Runnable task):代表没有返回值的异步任务
- 关闭线程池:
- public shutdown():启动一次顺序关闭,执行以前提交的任务,但不接收新任务
- public shutdownNow():立刻关闭
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ‐‐‐> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
//线程池只有两个线程对象,此任务会进入队列,等待下一轮执行
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池,执行完全部任务后关闭
//service.shutdown();
//执行两个任务就会关闭
//service.shutdownNow();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
//这里的sleep函数可以清楚地看到执行顺序
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,教完后,教练回到了游泳池");
}
}
10.10.定时器
- 定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式来执行。
- 定时器用来管理时间
- Timer触发执行的任务就是定时任务,即TimerTask
- 用来完成调度定时任务的功能
//创建定时器
Timer timer = new Timer()
//调度定时任务
//在指定的时间点,调度定时任务执行
schedule(TimerTask task, Date time)
//在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务
schedule(TimerTask task, long delay, long period)
//在firstTime时间点,首次执行,之后每period毫秒执行一次
schedule(TimerTask task, Date firstTime, long period)
//在delay毫秒的延时后,首次调度task执行,之后每period毫秒执行一次
scheduleAtFixedRate(TimerTask task, long delay, long period)
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");
- TimerTask(抽象类):表示待执行的定时任务
- 定义一个定时任务
- 继承TimerTask
- 重写run方法
- 终止定时器
- timer 当中的的cancle方法:定时器中所有任务都会被终止
public class Demo {
public static void main(String[] args) throws ParseException {
// /在指定的时间点,调度定时任务执行
//schedule(TimerTask task, Date time)
在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务
//schedule(TimerTask task, long delay, long period)
// 创建定时器
Timer timer = new Timer();
// 进行任务调度
String s = "2021-01-23 11:43:40";
// SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = simpleDateFormat.parse(s);
//timer.schedule(new MyTask(),date);
timer.schedule(new MyTask(),3000,5000);
timer.cancel();
}
}
/*
**如何定义一个定时任务**
- 继承TimerTask
- 重写run方法
* */
class MyTask extends TimerTask {
@Override
public void run() {
// 定时任务要做的事情写到run方法里
System.out.println("boom! 爆炸了!");
}
}
10.11.Callable
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
// 提交任务
Future future = pool.submit(new MyCallable());
//get() 获取计算结果
//如有必要,等待计算完成,然后获取其结果。
System.out.println("get before");
String s = (String) future.get();
System.out.println(s);
}
}
/*可以理解为多线程的实现方式3 ,但是必须要依赖线程池*/
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "------" +i);
}
Thread.sleep(3000);
return "hello";
}
}
- Runnable和Callable的区别:
- Callable规定的被重写的方法是call(),Runnable规定的被重写的方法是run()
- Callable的任务执行后可以有返回值,而Runnable的任务没有返回值
- call方法可以抛出异常,run方法不可以
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
10.12.volatile
- volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
- 特性:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
关于volatile原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步
- volatile写:当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。
- volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
- volatile指令重排:volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:
第一个操作/第二个操作 普通读写 volatile读 volatile写 普通读写 不允许 volatile读 不允许 不允许 不允许 volatile写 不允许 不允许
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/181089.html