文章目录
1 基本概念
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。
①若一个进程同一时间并行执行多个线程,就是支持多线程的。
②线程作为调度和执行的单位,每个线程拥有独立运行栈和程序计数器(pc),线程切换的开销小。
③一个进程(有一方法区、和堆)中的多个线程共享相同的内存单元/内存地址空间->它们从同意堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——生命周期 比如运行中的QQ
程序:是为了完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程任务。因为CPU时间单元特别短,因此感觉不出来。
如果是多核的话,才能更好的发挥多线程的效率。
一个Java应用程序java.exe 其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并发:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并行:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
为什么使用多线程(优点)?
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2.提高计算机系统CPU的利用率
3.改善程序结构。将即长又复杂的进程分为多个线程,独立运行,利于理解和修改。
什么时候使用多线程?
1.程序需要同时执行两个或多个任务。
2.程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
3.需要一些后台运行的程序时。
2 线程的创建和使用
创建新执行线程有两种方法。
- 继承Thread类
- 实现Runnable接口
2.1创建的多线程的方式一(继承Thread类):
- 声明为Thread子类
- 重写Thread类中的run()
- 创建Thread子类的对象
- 该对象调用start()
//声明为Thread子类
class MyThread extends Thread {
@Override
//重写Thread类中的run()
public void run() {
//遍历100以内的偶数
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) {
//创建Thread子类的对象
MyThread m1 = new MyThread();
//该对象调用start()
m1.start();
//m1.start(); //错误
}
}
思考第一个问题:
如果我想要创建两个线程可不可以直接通过此对象调用两次start()呢?
当你调用两次start()就会报 -> IllegalThreadStateException
为什么会报这样一个错误呢?
证据一: 当启动线程时,如果threadStatus!=0就会抛出异常。换言之,线程的状态码等于0,正常调用start()方法。
private volatile int threadStatus = 0;
public synchronized void start() {
//NEW状态 指的是创建一个新的线程
/**
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
}
证据二: 此方法的注释也有说明:如果当前线程已经启动,多次启动线程是不合法的
所以正确启动多个线程方式:
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
m1.start();
m2.start();
思考第二个问题:
此对象调用run()方法可以启动线程吗?
答曰:不可以
查看API文档可知start()方法的作用:
1.使该线程开始执行;
2.Java 虚拟机调用该线程的 run 方法。
验证:
通过获取当前线程名,就可以知道是否开启了新的线程
//稍微改造一下上面的代码
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
可以看到此对象调用start()方法,它的线程名是Thread-0,这就说明启动了线程
而线程名是main,这就说明了是主线程(主方法)调用了run()方法。
总结:
- 如果再启动一个线程,必须重新创建一个Thread子类的对象,不可以通过一个对象多次启动线程,会报错 IllegalThreadStateException。
- 线程的启动,必须调用start(),不能调用run()启动线程。
练习:创建两个线程 其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
public class PracticeThreadTest {
public static void main(String[] args) {
PraThread p1 = new PraThread();
PraThread2 p2 = new PraThread2();
p1.start();
p2.start();
}
}
class PraThread 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 PraThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2.2Thread类中常用的方法:
- start() 启动当前线程 调用当前线程run()
- run() 需要重写Thread类中run方法,将创建的线程的操作声明在此方法中
- currentThread() 静态方法:返回当前正在执行的线程
- getName() 获取当前线程的名字
- setName() 设置当前线程的名字 ①构造器设置名字 ②此对象.setName()
- yield() 释放当前CPU执行权
- join() 线程a中调用线程b的join(),此时线程a进入阻塞状态,等待线程b全部执行完,线程a才结束阻塞状态
- stop() 方式已过时,当执行此方法是,强制结束当前线程
- sleep() 让当前线程睡眠指定的millis 毫秒,在指定的毫秒时间内,该线程是阻塞状态
- isAlive() 判断当前线程是否存活
- getPriority() 获取线程优先级
- setPriority() 设置线程优先级
例子1:
两种方式修改当前线程的名字:
- 初始化父类有参构造器的name属性
- 调用setName()方法
class ThreadMethod extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
//方式一:
public ThreadMethod(String name) {
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
ThreadMethod t1 = new ThreadMethod("子线程");
//方式二:
//t1.setName("子线程");
t1.start();
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
例子2 测试yield():
测试结果: 正常情况来说两个线程是会交互的,多测试几次会明显的看到。
class ThreadMethod extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i % 20 == 0) {
//当前线程释放CPU执行权
yield();
}
}
}
public ThreadMethod() {
super("子线程");
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
ThreadMethod t1 = new ThreadMethod();
t1.start();
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
例子3 测试join():
class ThreadMethod extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public ThreadMethod(String name) {
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
ThreadMethod t1 = new ThreadMethod("子线程");
t1.start();
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i == 20) {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
执行结果:
例子4 测试sleep():
class ThreadMethod extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public ThreadMethod(String name) {
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
ThreadMethod t1 = new ThreadMethod("子线程");
t1.start();
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
例子5 测试isAlive()
class ThreadMethod extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public ThreadMethod(String name) {
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
ThreadMethod t1 = new ThreadMethod("子线程");
t1.start();
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i == 20) {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
boolean alive = t1.isAlive();
System.out.println(alive);
}
}
测试结果:
说明当前线程执行完声明的逻辑后,就死亡了。
false
2.3线程优先级
例子 测试:
- setPriority()方法
- getPriority()方法
Thread类中分别定义了最小、正常、最大优先级。
其中默认的优先级是NORM_PRIORITY = 5。
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(getName()+ ":" + getPriority() +":" + i);
}
}
}
public MyThread(String name) {
super(name);
}
}
public class ThreadPriorityTest {
public static void main(String[] args) {
MyThread m1 = new MyThread("子线程");
//设置子线程的优先级
m1.setPriority(Thread.MAX_PRIORITY);
//启动线程 调用run()
m1.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);
}
}
}
}
说明:高优先级的线程不一定要执行完,才能执行低优先级的线程,只是高优先级的线程抢占CPU执行权的概率高于低优先级的线程
练习:模拟窗口买票,创建三个窗口买票 总票数100张 使用继承Thread类
代码如下:
class Window extends Thread{
//三个窗口(线程)共享100张票
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
//这里调用sleep()目的是提高重票和错票的概率
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ": 卖票,票号为" + ticket);
ticket--;
}else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
出现线程安全问题(留到线程同步时候说明):
2.4创建的多线程的方式二(实现Runnable接口):
- 创建一个实现Runnable接口的类
- 实现类去实现Runnable中的抽象方法 run()
- 创建实现类的对象
- 该此对象作为参数传入Thread类的构造器中,创建Thread对象
- 通过Thread类的对象调用start()
//1. 创建一个实现Runnable接口的类
class MyThread2 implements Runnable{
//2. 实现类去实现Runnable中的抽象方法 run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
//3. 创建实现类的对象
MyThread2 m1 = new MyThread2();
//4. 该此对象作为参数传入Thread类的构造器中,创建Thread对象
Thread t1 = new Thread(m1);
//5. 通过Thread类的对象调用start() ①启动线程 ②调用当前线程run() -> 调用Runnable类型的target run()
t1.start();
}
}
为什么要将实现类的对象作为参数Thread类的构造器中,创建Thread对象?
源码:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
可以看见Thread类中,不止通过无参构造一种方式去创建Thread类对象,还可以通过传入一个target参数去创建Thread类对象,在这里我们直接传入Runnable接口的子类去完成创建。
谁调动了run方法?
继承Thread类,调用start()方法,不仅可以启动线程,而且也调用了run()。
实现Runnable接口的子类,调用start()方法,启动线程后,是target调用了run()方法
源码:
@Override
public void run() {
//已经创建了一个实现Runnable接口的类,所以target不等于null,满足条件。
if (target != null) {
target.run();
}
}
练习:模拟窗口买票,创建三个窗口买票 总票数100张 创建实现Runnable接口的类。
代码如下:
class Window implements Runnable{
//这里区别于继承Thread类 不加static关键字 因为创建了一个对象本身就只有一份ticket属性
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为" + ticket);
ticket--;
}else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
出现线程安全问题(留到线程同步时候说明):
两种创建多线程方式的比较
开发中:优先选择 实现Runnable接口的方式
原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程共享数据的情况
联系:Thread类实现了Runnable接口 -> public class Thread implements Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
3 线程的生命周期(状态)
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
JDK中用Thread.State类定义了线程的几种状态。
新建状态(NEW)
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
就绪状态(RUNNABLE)
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
运行状态(RUNNING)
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞状态(BLOCKED)
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运(running)状态。阻塞的情况分三种:
一.等待阻塞(o.wait->等待对列):
- 运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
二.同步阻塞(lock->锁池)
- 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
三.其他阻塞(sleep/join)
- 运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O
处理完毕时,线程重新转入可运行(runnable)状态。
死亡状态(DEAD)
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束
- run()或 call()方法执行完成,线程正常结束。
异常结束
- 线程抛出一个未捕获的 Exception 或 Error。
调用stop()
- 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
图解:
4 线程的同步
问题:多个线程操作同一个数据时出现重票、错票问题。
出现问题原因:当某个线程操作车票的过程中,尚未完成,另一个线程也参与进来,对车票进行操作。
解决方式:当一个线程a在操作ticket的时候,其他线程不能参与进来。 直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能改变。
Java中通过同步机制,解决线程安全问题。
解决方式:
//语法
synchronized(同步监视器) {
//需要被同步的代码
}
什么是同步监视器?
答曰:同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
什么是需要被同步的代码?
①操作共享数据的代码,即为需要被同步的代码
②共享数据:多个线程共同操作的变量,比如:ticket就是共享数据。
使用同步代码块解决窗口售票出现的多线程安全问题。
代码如下(继承Thread类):
class Window extends Thread {
//多个线程共享一个变量
private static int ticket = 100;
//多个线程共用同一个属性
private static Object obj = new Object();
@Override
public void run() {
while (true) {
//同步监视器的两种方式:①类.class ②多个线程通用同一个对象属性。
//synchronized (Window.class) {
synchronized (obj) {
if (ticket > 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ": 卖票,票号为" + ticket);
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
执行结果:
成功解决线程安全问题。
代码如下(实现Runnable接口的类):
class Window2 implements Runnable {
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
//this指的是当前对象的引用 当前对象只有一个,引用也就有一个,符合同步监视器的条件
//synchronized (this) {
synchronized (obj) {
if (ticket > 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 w1 = new Window2();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
//方法上加synchronized关键字
public synchronized int length() {
return count;
}
同步方法: 也需要同步监视器,只是不需要我们显式声明
静态同步方法:同步监视器指的是当前类本身
非静态同步方法:同步监视器指的是 this
使用同步方法解决窗口售票出现的多线程安全问题。
代码如下(继承Thread类):
class Window3 extends Thread {
//多个线程共享一个变量
private static int ticket = 100;
@Override
public void run() {
while (true) {
play();
}
}
public static synchronized void play() {
if (ticket > 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Window3 w2 = new Window3();
Window3 w3 = new Window3();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
代码如下(实现Runnable接口的类):
class Window4 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
play();
}
}
public synchronized void play() {
if (ticket > 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
1.从JDK5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
2.java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
3.ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显式加锁、释放锁。
4.通过有参构造创建ReentrantLock对象,其中参数fair表示公平。如果参数为true,则表示线程公平竞争,根据线程启动的顺序去执行。参考下面的代码的结果可知。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
使用Lock解决窗口售票出现的多线程安全问题。
class LockThread implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
//锁定方法
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 {
//解锁方法
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
LockThread lockThread = new LockThread();
Thread t1 = new Thread(lockThread);
Thread t2 = new Thread(lockThread);
Thread t3 = new Thread(lockThread);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t3.start();
t2.start();
t1.start();
}
}
执行结果:
综合练习:
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
分析:
是否存在多线程? 是
是否存在共享数据? 是 两个用户都操作账户
是否存在安全问题? 是
代码如下:
//账户
class Account {
//账户余额
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱的方法
public void deposit(double money) {
synchronized (this) {
if (money > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance += money;
System.out.println(Thread.currentThread().getName() + ": 存钱,余额为" + balance);
}
}
}
}
//储户
class Consumer implements Runnable {
//共享数据
private Account account;
public Consumer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acc = new Account(0);
Consumer con = new Consumer(acc);
Thread t1 = new Thread(con);
Thread t2 = new Thread(con);
t1.setName("甲");
t2.setName("乙");
t1.start();
t2.start();
}
}
执行结果:
面试题:synchronized 与 Lock的异同?
- 相同:都是解决线程安全问题
- 不同: synchronized机制在执行完相应的代码块以后,自动的释放同步监视器
- Lock需要手动的启动同步锁(lock())、同时手动的释放锁(unLock())
4.4死锁问题:
死锁:
不同线程占用了对方需要的同步资源,都在等待对方放弃同步资源,就形成了线程的死锁。
简单来说:多个线程互相抱着对方需要的资源,然后形成僵持。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法:
专门的算法、原则。
尽量减少同步资源的定义。
尽量避免嵌套同步
死锁的例子1
public class DeadLockTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
//假设有线程a和线程b 线程一启动,如果
//线程a拿到了s1锁,这时候睡了一秒 ,阻塞的过程中,很有可能线程b拿到了s2锁,
//这时候线程b也睡了一秒,想要继续执行下去,必须拿到s1锁 发现s1锁已经被线程a拿到 无法执行下去
//这时候就出现了 双方都占用这对方需要的共同资源,互相僵持这,出现 死锁。
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
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 {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
}
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}
死锁的例子2:
//口红
class Lipstick {
}
//镜子
class Mirror {
}
class Makeup extends Thread {
int choice;//选择
String name; //化妆人的姓名
//只有一份镜子和口红
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
public Makeup(int choice, String name) {
this.choice = choice;
this.name = name;
}
@Override
public void run() {
makeUp();
}
public void makeUp() {
if (choice == 0) {
synchronized (lipstick) {
System.out.println(this.name + "拿到了口红");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mirror) {
System.out.println(this.name + "拿到了镜子");
}
}
}else {
synchronized (mirror) {
System.out.println(this.name + "拿到了镜子");
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lipstick) {
System.out.println(this.name + "拿到了口红");
}
}
}
}
}
public class DeadLockTest2 {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"灰姑娘");
Makeup g2 = new Makeup(1,"白雪公主");
g1.start();
g2.start();
}
}
解决方式:同步代码块不要嵌套。
面试题:synchronized 与 Lock的异同?
- 相同:都是解决线程安全问题
- 不同: synchronized机制在执行完相应的代码块以后,自动的释放同步监视器
Lock需要手动的启动同步锁(lock())、同时手动的释放锁(unLock())
5 线程的通信
线程通信的例子:使用两个线程打印 1-100。线程1,线程2 交替打印
涉及到的三个方法:
- wait(): 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就会唤醒优先级高的
- notifyAll(): 一旦执行此方法,就会唤醒所有被wait的线程
说明:
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器
否则,会出现IllegalMonitorStateException异常 - wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
代码如下:
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while (true) {
/**
* 解释 wait() notify()的过程
* 当CPU将执行权分配给线程a时,线程a进入同步代码块(此时其他线程进不来)执行逻辑,
* 当线程a调用wait()方法,线程a进入阻塞状态,并且释放同步监视器(也就是同步锁),这时CPU将执行权分配给了线程b
* 线程b进入了同步代码块(锁好门),这时执行notify()唤醒线程a,线程a只能在同步代码块外面等着。
*
*/
//同步监视器可以是任何类的对象 此对象调用notify() 说明notify()被放在Object类中
synchronized (obj) {
obj.notify();
if (number <= 100) {
try {
Thread.sleep(100);
} 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();
}
}
执行结果:
面试题:sleep() 和 wait()的异同?
- 相同点:一旦执行放,都可以使得当前线程进入阻塞状态。
- 不同点:
①两个方法声明在不同类中:Thread类中声明静态的sleep(),Object类中声明wait()
②调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须宰同步代码块或同步方法中调用
③关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
生产者和消费者例题:
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来去走产品。
分析:
1.是否是多线程问题? 是,生产者线程,消费者线程
2.是否有共享数据?是店员(或产品)
3.如何解决线程的安全问题?同步机制,有三种方法
4.是否涉及线程的通信?是
代码如下
//店员
class Clerk {
//产品数量
private int productCount;
//生产产品的方法
public synchronized void producerProduct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费产品的方法
public synchronized void consumerProduct() {
if (productCount > 0) {
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
class Consumer extends Thread {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("消费者开始消费产品....");
while (true) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumerProduct();
}
}
}
//生产者
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("生产者开始生产产品....");
while (true) {
try {
sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.producerProduct();
}
}
}
public class ProducerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
Consumer consumer2 = new Consumer(clerk);
producer.setName("生产者1");
consumer.setName("消费者1");
consumer2.setName("消费者2");
producer.start();
consumer.start();
consumer2.start();
}
}
6 JDK5.0新增线程创建方式
- 创建实现Callable接口的实现类
- 完成Callable接口的call()重写
- 创建接口的实现类对象
- 该对象作为参数传递给FutureTask类的构造器, 创建该类对象
- 此对象作为参数传递给Thread类的构造器,创建一个线程
- 调用futureTask的get()返回重写call()的返回值
//1. 创建实现Callable接口的实现类
class MyThread implements Callable<Integer>{
//2. 完成Callable接口的call()重写
@Override
public Integer call() throws Exception {
//完成100以内的偶数之和
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 创建接口的实现类对象
MyThread m1 = new MyThread();
//4. 该对象作为参数传递给FutureTask类的构造器, 创建该类对象
FutureTask<Integer> futureTask = new FutureTask<Integer>(m1);
//5. 此对象作为参数传递给Thread类的构造器,创建一个线程
Thread t1 = new Thread(futureTask);
t1.start();
try {
//6. 调用futureTask的get()返回重写call()的返回值
Integer sum = futureTask.get();
System.out.println("偶数总和: " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
理解为什么通过FutureTask类的对象去创建线程。
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。比如说:去一个地方你选择骑车去,应该避免自己去造车子,到达目的地后销毁这样的事情。应该去选择共享单车。
好处:
①提高响应速度(减少了创建新线程的时间)
②降低资源消耗(重复利用线程池中线程,不需要每次都创建)
③便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
代码如下:
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);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//获得操作线程管理的类
ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) service;
//设置核心池的大小
poolExecutor.setCorePoolSize(20);
//2. 执行指定的线程操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适用于Runnable
//service.submit();//适用于Callable
//关闭连接池
service.shutdown();
}
}