一:什么是线程?
线程(英语:thread)是操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,
一个进程中可以并发多个线程,每条线程并行执行不同的任务。
二:创建多线程方式的五种方式:
1、继承Thread类
- 1:创建一个Thread类的子类
- 2:在子类中重写run方法
- 3:创建子类对象
- 4:通过子类对象调用start方法
- 5:在main方法里面写一个与之比较
public class ThreadDemo1 extends Thread{ //1:创建一个Thread类的子类
public void run() { //2:在子类中重写run方法
for (int i = 0; i < 5; i++) {
System.out.println("重写的run方法:"+i);
}
}
public static void main(String[] args) {
ThreadDemo1 t1 = new ThreadDemo1(); //3:创建子类对象
t1.start(); //4:通过子类对象调用start方法
for (int i = 0; i<5; i++) {
System.out.println("main线程:"+i); //5:在main方法里面写一个与之比较
}
}
}
运行结果:
2、实现Runnable接口
- java.lang.Runnable
- Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。
- 类必须定义一个称为 run 的无参数方法。
- java.lang.Thread类的构造方法
- Thread(Runnable target) 分配新的 Thread 对象。
实现步骤:
- 1:创建一个Runnable接口的实现类
- 2:在实现类中重写Runnable接口中的run()方法,设置线程任务
- 3:创建一个Runnable接口实现类的对象
- 4:创建Thread对象,调用Thread类的构造器Thread(Runnable target) ,将实现类的对象作为参数
- 5:调用Thread类中的start方法,开启新的线程执行run()方法
public class ThreadDemo3 {
class RunnableImp implements Runnable{ //1:创建一个Runnable接口的实现类
@Override
public void run() { //2:在实现类中重写Runnable接口中的run()方法,设置线程任务
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
RunnableImp demo3 = new RunnableImp(); //3:创建一个Runnable接口实现类的对象
Thread th = new Thread(demo3); //4:创建Thread对象,调用Thread类的构造器Thread(Runnable target) ,将实现类的对象作为参数
th.start(); //5:调用Thread类中的start方法,开启新的线程执行run()方法
for (int i = 0; i <5; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}
运行效果:
3、实现callable接口 jdk5.0之后
与runnable接口的比较优势?
- ①:call()方法是有返回值的
- ②:call()方法是可以抛出异常的,被外面的操作捕获,获得异常的信息
- ③:call()方法支持泛型
步骤:
- 1:创建一个实现callable的实现类
- 2:实现(重写) call方法,将此线程需要执行的操作声名在call()中
- 3:创建callable接口实现类的对象
- 4:将此callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask对象
- 5:将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 6:获取callable中call方法的返回值
//1:创建一个实现callable的实现类
class NumThread implements Callable{
//2:实现(重写) call方法,将此线程需要执行的操作声名在call()中
public Object call() throws Exception{
int sum = 0;
for(int i=1;i<=100;i++) {
if(i%2==0) {
sum +=i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3:创建callable接口实现类的对象
NumThread n = new NumThread();
//4:将此callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask对象
FutureTask futuretask = new FutureTask(n);
//5:将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futuretask).start();
try {
//get()返回值即为FutureTask构造器里面参数实现callable类重写call()的返回值
//6:获取callable中call方法的返回值
Object sum = futuretask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
拓展一下:
我们点进源码探究一下…
发现他传给 thread 类的是这个构造器方法,咦~ 不是说 FutureTask 吗?怎么是 Runnable
其实这时候我们心里应该有感觉 这个 FutureTask 应该是 runnable的子类了…
结论:FutureTask implements RunnableFuture extends Runnable
4、使用匿名内部类
- 匿名:没有名字
- 内部类:写在其他类内部的类
匿名内部类的作用:简化代码
- 把子类继承父类,重写父类方法,创建子类对象 一步完成
- 把实现类 实现接口,重写接口中的方法,创建实现类的对象 一步完成
匿名内部类的最终产物: 子类/实现类对象,而这个类没有名字
格式:
new 父类/接口(){
重写父类/接口的方法
};
测试代码
//把子类继承父类(Thread),重写父类方法,创建子类对象
new Thread() {
//重写run()方法,设置线程任务
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}.start();
//把实现类 实现接口(Runnable),重写接口中的方法,创建实现类的对象
new Thread(new Runnable() {
//重写接口中的方法,设置线程任务
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
).start();
运行结果:
5、使用线程池 jdk1.5之后
java.util.concurrent包下 类 Executors -->public class Executors 线程池的工具类,用来生成线程池
Executors类中的静态方法
* static ExecutorService newFixedThreadPool(int nThreads)
* 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
* 参数:int nThreads -->创建线程池中包含线程的数量
* 返回值:ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用 ExecutorService接口接收(面向接口编程)
java.util.concurrent包下 接口 ExecutorService -->线程池接口
- 用来从线程池中提取线程,调用start方法,执行线程任务
- submit(Runable task) 提交一个 Runnable任务用于执行
- 关闭,销毁线程池的方法
- void shutdown()
线程池的使用步骤:
- 1:使用线程池的工厂类Executors里边提供 的静态方法 newFixedThreadPool 生产一个指定线程数量的线程池
- 2:创建一个类,实现Runnable接口或Callable接口,重写run方法,设置线程任务
- 3:调用ExecutorService接口的submit(),传递线程任务(实现类),开启线程,执行run方法
- 4:调用ExecutorService接口的shutdown()销毁线程池(一般不建议执行)
常用参数:
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
好处:
- 1:提高响应速度(减少了创建新线程的时间)
- 2:降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 3:便于线程管理
代码:
public class ThreadDemo10_ThreadPool {
public static void main(String[] args) {
//1:使用线程池的工厂类Executors里边提供的静态方法 newFixedThreadPool 生产一个指定线程数量的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);//返回一个接口
//3:调用ExecutorService接口的submit(),传递线程任务(实现类),开启线程,执行run方法
/*Runnable10 run = new Runnable10();
pool.submit(run);*/
//线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
pool.submit(new Runnable10());
pool.submit(new Runnable10());
pool.submit(new Runnable10());
pool.submit(new Runnable10());
pool.submit(new Runnable10());
//4:调用ExecutorService接口的shutdown()销毁线程池(一般不建议执行)
// pool.shutdown();
}
}
//2:创建一个类,实现Runnable接口,重写run方法,设置线程任务
class Runnable10 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 创建了一个新线程");
}
}
拓展一下:
为什么execute()方法 适合runnable呢?
看源码~
为什么submit()适合callable呢?
看源码~
关于线程创建的五种方式图解
三:多线程的安全问题
线程安全问题引入
三个窗口卖出同一张票的事故 以及卖出不存在的票
卖票案例
创建3个线程,同时开启,对共享的票进行出售
public class ThreadDemo5_Safe {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Runnable impl = new RunnableImpl();
//创建Thread对象,构造方法中传递Runnable接口的实现类对象
Thread t1 = new Thread(impl);
Thread t2 = new Thread(impl);
Thread t3 = new Thread(impl);
t1.start();
t2.start();
t3.start();
}
}
class RunnableImpl implements Runnable{
//定义 一个共享的数据:票
private int ticket=100;
//设置线程任务:卖票
@Override
public void run() {
//使用循环卖票 判断是否有票
while(ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖:"+ticket);
ticket--;
}
}
}
问题漏洞:
如何解决线程安全问题? 有三种方式
1:同步代码块
格式
synchronized(锁对象) {
放置可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
- 1:通过代码块中的锁对象,可以使用任意的对象
- 2:必须保证多个线程使用同一个锁对象
- 3:锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行
代码:
//实现Runnable接口
class RunnableImpl6 implements Runnable{
//定义票的总数
private int ticket = 100;
//定义一个锁
Object obj = new Object();
//重写run()方法
@Override
public void run() {
while(ticket>0) {
//同步代码块 每次只能让一个线程进来 所以后面还需要加上一个判断票数
synchronized(obj) {
//需判断票是否有剩
if(ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
}
}
}
}
}
public class ThreadDemo6_SolveSafe {
public static void main(String[] args) {
//创建接口类的实现类的对象
RunnableImpl6 impl = new RunnableImpl6();
//创建Thread的对象
Thread t1 = new Thread(impl);
Thread t2 = new Thread(impl);
Thread t3 = new Thread(impl);
t1.start();
t2.start();
t3.start();
}
}
运行结果:
2:同步方法
格式
修饰符 synchronized 返回值类型 方法名(参数列表){
有关线程安全问题的代码(访问了共享数据的代码)
}
使用步骤:
- 1:把访问了共享数据的代码抽取出来 放进一个方法里面
- 2:在方法上添加synchronized修饰符
写好同步方法之后,在实现类重写run()方法里面调用即可
代码:
class Runnable7 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(ticket>0) {
TongBuMethod();
}
}
/*定义一个同步方法
* 同步方法也会把方法内部的代码锁住,只让一个线程进行
* 同步方法的锁对象就是实现类对象 new Runnable7() 也就是this
* */
public synchronized void TongBuMethod() {
if(ticket>0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
}
}
}
public class ThreadDemo7_SolveSafe {
public static void main(String[] args) {
Runnable7 impl = new Runnable7();
Thread t1 = new Thread(impl);
Thread t2 = new Thread(impl);
Thread t3 = new Thread(impl);
t1.start();
t2.start();
t3.start();
}
}
/*定义静态同步方法
* 锁对象就不是this了,this是创建对象之后产生的,静态方法优先于对象
* 静态方法的锁对象是本类的class属性 --> class文件对象
* */
public static synchronized void StaticTongBuMethod() {
if(ticket>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket --;
}
}
}
关于同步方法的总结:
- 1:同步方法仍然涉及到同步监视器,只是不需要我们显式的声名
- 2:非静态的同步方法,同步监视器是this
- 3:静态的同步方法,同步监视器是 当前类本身
- 其实就是看锁是否唯一
3;Lock锁机制 jdk5.0之后
异同:
- 相同:都能解决线程的安全问题
- 不同:synchronized 自动释放同步监视器、lock需手动启动同步与结束同步
- Lock是显式锁(手动开启,手动关闭) 只有代码块锁
- synchronized是隐式锁,出了作用域自动释放 有代码块锁和方法锁
优先原则:
- 使用lock锁,JVM将花费较少时间来调度线程,性能更好
- 优先使用顺序:lock ——> 同步代码块 ——>同步方法
java.util.concurrent.locks.Lock接口
- Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作
- 使用Lock接口中的两个方法:lock()和unlock()
注意:既然Lock是个接口 就需要用到他的实现类:ReentrantLock
- java.util.concurrent.locks.ReentrantLock
使用步骤:
- 1:在成员变量位置创建一个ReentrantLock对象
- 2;在可能出现线程安全问题的代码前 调用Lock接口中的 Lock()获取锁
- 3:在可能出现线程安全问题的代码后 调用Lock接口中的 unLock()释放锁
代码:
public class ThreadDemo8_SolveSafe {
public static void main(String[] args) {
Runnable8 r = new Runnable8();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
class Runnable8 implements Runnable{
public int ticket = 100;
// 1:在成员变量位置创建一个ReentrantLock对象
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(ticket>0) {
// 2;在可能出现线程安全问题的代码前 调用Lock接口中的 Lock()获取锁
lock.lock();
if(ticket>0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 3:在可能出现线程安全问题的代码后 调用Lock接口中的 unLock()释放锁
lock.unlock();
}
//最好把释放锁的代码 放在异常捕获的finally语句中,那么无论是否发生异常,锁都会进行释放
}
}
}
}
解决线程安全问题的三种方式
四:线程的死锁问题
关于死锁的理解:
- 不同的线程分别占用对方需要的同步资源不放弃
- 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
死锁现象说明:
- 出现死锁后,不会出现异常,不会 出现提示,只是所有的线程都处于阻塞状态,无法继续
- 我们使用同步时,要避免使用死锁
代码:
public class DeadLock {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
// 匿名内部类 继承Thread
new Thread() {
public void run() {
synchronized(s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
}catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
//匿名内部类 实现runnable接口
new Thread(new Runnable() {
public void run() {
synchronized(s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
}catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}) .start();
}
}
运行结果:
线程死锁图解
五:线程通信
关于线程通信的三个方法:
- wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器
- notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,则唤醒优先级最高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
注意点:
- wait(),notify(),notifyAll()是三个线程通信的方法
- 使用这三个方法的前提:只能出现在同步代码块 或者 同步方法中
- wait(),notify(),notifyAll()三个方法的调用者 必须是同步代码块或同步方法中的同步监视器(锁)
- 否则,会报错IllegalMonitorStateException异常
小扩展:
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
- 因为所有对象都能作为同步监视器,而同步监视器能调用这三个方法,所以这三个方法属于java.lang.Object类、
代码:
class Number implements Runnable{
public int num =1;
public Object obj = new Object();
public void run() {
while(true) {
//同步代码块 同步监视器为this -> Number
//可将this 改为obj 那么下面就得使用obj调用notify()和wait()方法
synchronized(this) {
//把当前wait的线程唤醒 虽然唤醒,
//但是锁已被占用,即使唤醒也无法进来
//此时省略的是this.(非静态方法) 如果是静态方法,则默认是调用Number.
notify();
if(num<=10) {
System.out.println(Thread.currentThread().getName()+":"+num);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
try {
//使调用wait()方法的进程进入阻塞状态 释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
public class CommunicateTest {
public static void main(String[] args) {
Number num = new Number();
Thread t1 = new Thread(num);
Thread t2 = new Thread(num);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
运行结果:
进入到TimeWaiting(计时等待) 有两种方式
- sleep(long timeout)
- 在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
- void wait(long timeout)
- wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
- 类似sleep方法
唤醒方法有两个
- notify()唤醒在此对象监视器上等待的单个线程
- notifyAll()唤醒在此对象监视器上等待的全部线程
sleep()和wait()的异同
- 相同点:一旦执行此方法,都可以使得当前线程进入阻塞状态
- 不同点:
- 两个方法声名的位置不同: Thread类中声名sleep(),Object类中声名wait()
- 调用的要求不同,sleep可以在任何场景使用, wait必须在同步代码块或同步方法中使用
- 关于释放同步监视器:如果两个方法都在同步代码块或同步方法中
- sleep不释放同步监视器
- wait释放同步监视器
等待唤醒案例:线程之间的通信
- 创建一个消费者:我手机爆屏了,快帮我修理!
- 然后调用wait方法,放弃对CPU的争抢,进入到waiting状态(无限等待)
- 创建一个生产者:花3秒修理屏幕
- 修理好之后 ,调用notify方法,唤醒顾客拿手机
注意:
- 此时顾客和店家必须用同步代码块包起来,保证等待和唤醒只能有一个在执行
- 同步使用的锁对象必须保证唯一
- 只有锁对象才能调用wait和notify方法(就是Object类中的方法)
public class ThreadDemo9_WaitAndNotify {
public static void main(String[] args) {
//创建锁对象
Object obj = new Object();
//创建顾客线程 使用匿名内部类
new Thread() {
public void run() {
while(true) {
//保证等待和唤醒只能有一个在执行 使用同步代码块
synchronized(obj){
System.out.println("我手机爆屏了,快帮我修理!");
//调用wait方法,放弃对CPU的争抢,进入到waiting状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//被唤醒后会继续执行wait之后的语句
System.out.println("谢谢老板,老板身体健康!");
}
}
}
}.start();
//创建店家线程 使用匿名内部类
new Thread() {
public void run() {
while(true) {
//先睡三秒 放弃对CPU的争夺 让顾客先抢到
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj) {
//修理好之后 ,调用notify方法,唤醒顾客拿手机
System.out.println("花3秒修理屏幕... 你过来拿手机");
obj.notify();
}
}
}
}.start();
}
}
运行结果:
再次感受一下 notify()和notifyAll()的区别~
两个顾客 一个店家举例
public class ThreadDemo9$5 {
public static void main(String[] args) {
//创建锁对象
Object obj = new Object();
//创建顾客线程 使用匿名内部类
new Thread() {
public void run() {
while(true) {
//保证等待和唤醒只能有一个在执行 使用同步代码块
synchronized(obj){
System.out.println("我手机爆屏了,快帮我修理!");
//调用wait方法,放弃对CPU的争抢,进入到waiting状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//被唤醒后会继续执行wait之后的语句
System.out.println("手机修好了~谢谢老板,老板身体健康!");
}
}
}
}.start();
//创建顾客线程 使用匿名内部类
new Thread() {
public void run() {
while(true) {
//保证等待和唤醒只能有一个在执行 使用同步代码块
synchronized(obj){
System.out.println("我笔记本爆屏了,快帮我修理!");
//调用wait方法,放弃对CPU的争抢,进入到waiting状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//被唤醒后会继续执行wait之后的语句
System.out.println("笔记本修好了~恭喜发财,老板万事如意!");
}
}
}
}.start();;
//创建店家线程 使用匿名内部类
new Thread() {
public void run() {
while(true) {
//先睡三秒 放弃对CPU的争夺 让顾客先抢到
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj) {
//修理好之后 ,调用notify方法,唤醒顾客拿手机
System.out.println("花3秒修理屏幕... 你过来拿");
obj.notify(); //唤醒一个
// obj.notifyAll();
}
}
}
}.start();
}
}
运行结果:
线程通信图解
六:线程状态(6种)
阻塞状态:具有CPU的执行资格,等待CPU空闲时执行
休眠状态:放弃CPU的执行资格,CPU空闲,也不执行