1. 概念
- 进程:当一个程序正在运行时,就是一个进程
- 线程:进程中的一个执行单元,负责控制进程的执行
- 线程分为用户线程和守护线程
一个程序至少有一个进程,一个进程可以有多个线程
2. 普通方法调用和多线程的区别
- 普通方法的执行
- 多线程的执行
3. 实现多线程的三种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
3.1 Thread类
优点:编写简单,想要访问当前线程直接this就可以获取
缺点:线程类无法继承其他类
步骤:
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
//继承Thread类,重写run()方法,调用start()方法
public class TestThread extends Thread {
@Override
public void run() {
//run方法线程体
for (int i = 0; i <10 ; i++) {
System.out.println("学习");
}
}
public static void main(String[] args) {
//main线程,主线程
TestThread testThread =new TestThread();
testThread.start();
for (int i = 0; i <200 ; i++) {
System.out.println("写代码");
}
}
虽然在代码中是先让start()方法先执行的,但是输出的结果是main主线程先输出;
run()方法的输出结果穿插在中间,并且每次输出的位置不一样;
所以,线程开启时不一定会立即执行,由cpu调度执行
3.2 Runnable接口
优点:实现接口类,还可以继承其他类
缺点:编写稍复杂,如果想访问当前线程需要使用Thread.currentThread()方法
步骤:
- 实现Runnable接口
- 重写run()方法
- new Thread类,并把runnable接口实现类当作参数
- thread.start()启动线程
//Runnable接口,重写run()方法,执行线程需要丢入runnable接口实现类
public class MyRunnable implements Runnable {
public void run() {
//run线程
for (int i = 0; i <10 ; i++) {
System.out.println("学习");
}
}
public static void main(String[] args) {
//创建runnable接口实现类对象
MyRunnable myRunnable=new MyRunnable();
//创建线程对象,通过线程对象来开启线程
Thread thread =new Thread(myRunnable);
thread.start();
for (int i = 0; i <100 ; i++) {
System.out.println("写代码");
}
}
}
3.3 Callable接口
与Runnable接口不同点:
- Runnable的重写方法是run(),Callable重写方法是call()
- Callable有返回值,Runnable没有
- call方法可以抛异常
步骤:
- 实现Callable接口,需要定义泛型类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(线程数量);
- 提交执行:Futrue<泛型类型> r1 = ser.submit(目标对象);
- 获取结果:boolean rs1 = r1.get();
- 关闭服务:ser.shutdownNow();
注:(ExecutorService
是一个线程池接口,Executors有四种线程池)
public class MyCallable implements Callable<Boolean> {
public MyCallable(String url,String file) {
this.url=url;
this.file=file;
}
String url;
String file;
//上面不用管,看下面的
//call方法相当于Runnable接口中的run方法
public Boolean call() throws Exception {
WebDowload webDowload=new WebDowload(url,file);
webDowload.dowloader();
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建目标对象
MyCallable myCallable1 =new MyCallable("https://i0.hdslb.com/bfs/archive/622017dd4b0140432962d3ce0c6db99d77d2e937.png","D://1.png");
MyCallable myCallable2 =new MyCallable("https://i0.hdslb.com/bfs/archive/622017dd4b0140432962d3ce0c6db99d77d2e937.png","D://2.png");
MyCallable myCallable3 =new MyCallable("https://i0.hdslb.com/bfs/archive/622017dd4b0140432962d3ce0c6db99d77d2e937.png","D://3.png");
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> r1 = ser.submit(myCallable1);
Future<Boolean> r2 = ser.submit(myCallable2);
Future<Boolean> r3 = ser.submit(myCallable3);
//获取结果
boolean rs1 =r1.get();
boolean rs2 =r2.get();
boolean rs3 =r3.get();
//关闭服务
ser.shutdownNow();
}
}
4. 线程状态
-
创建状态:生成线程对象,没有启动,这是创建状态
-
就绪状态:线程对象在start开启之后就处于就绪状态,如果线程对象从等待或者睡眠结束之后也会变成就绪状态
-
运行状态:线程调度程序将处于"就绪状态" 的线程对象设置为当前线程,此时状态就变成了运行状态,并运行run()中的代码
-
阻塞状态:线程在运行时被暂停,sleep、suspend、wait等方法都可以导致线程阻塞
-
死亡状态:线程在run()方法结束之后,或者调用stop方法之后就进入死亡状态
阻塞的5种状态:
1. 线程休眠: Thread.sleep(long millis)
参数为毫秒数
- sleep(时间)指定当前线程的毫秒数;
- sleep存在异常InterruptedException;
- sleep时间达到后线程进入就绪状态;
- sleep可以模拟网络延时,倒计时等;
- 每一个对象都有一个锁,sleep不会释放锁;
2. 线程礼让: Thread.yield();
让别的线程先运行,让当前线程暂停从运行状态变成就绪状态,但不进入阻塞状态,礼让不一定成功
3. 合并线程: thread.join();
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞,可以想象成插队
4.改变线程优先级:getPriority();
获取线程优先级,返回值为int,setPriority(int i);
设置线程优先级,参数为int
线程优先级最大值为10,最小值为1,标准值为5;
优先级低,不代表不会被调用,只是调用的概率变低了,都得看cpu的调度;
(注意要先设置优先级,再start)
public class TestPriority {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1启动");
},"线程1");
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2启动");
},"线程2");
//设置线程优先级
thread1.setPriority(1);
thread1.start();
System.out.println(thread1.getPriority());
thread2.setPriority(10);
thread2.start();
System.out.println(thread2.getPriority());
}
}
5.将线程设置成守护线程:thread.setDaemon(Boolean on)
设置线程为守护线程,参数为布尔类型,true为设置成守护
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如,后台记录操作日志,内存监控,垃圾回收等
当用户线程结束之后,守护线程也会自动结束
4.1 观测线程状态
Thread.State state = thread.getState();
创建观测状态
state=thread.getState();
更新状态,进入下一种状态的时候再获取一次
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread( () -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("/");
});
//观察线程状态
//New
Thread.State state = thread.getState();//创建观测状态
System.out.println(state);
//启动线程
thread.start();
//Run
state=thread.getState(); //更新状态
System.out.println(state);
while (state!=Thread.State.TERMINATED) {//只要线程不终止,就一直输出状态
Thread.sleep(100);
state=thread.getState();//更新状态
//Terminated,线程终止
System.out.println(state);
}
}
}
几种线程状态的输出结果:
- new: new Thread() 后处于新生状态,尚未启动
- Runnable: 在Java虚拟机中执行的线程处于此状态
- Blocked: 被阻塞,等待监视器锁定的线程处于此状态
- Waiting: 等待另一个线程执行特定动作的线程处于此状态
- Time_Waiting: 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- Terminated: 退出的线程处于此状态
5. synchronized
使用线程同步可以保证安全性,但是会导致性能降低的问题
比如:
- 一个线程持有锁会导致其他需要此锁的线程全部挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时
- 如果优先级较高的线程等待一个优先级较低线程释放锁,会导致优先级倒置
synchronized可以使用在方法上和代码块中;
作用在方法上:
锁类的实例对象;
private synchronized void method(){}
锁类对象(即new多个实例对象,但他们仍然是属于同一个类依然会被锁住);
private static synchronized void method(){}
作用在代码块中:
锁类的实例对象;
synchronized(this){
}
锁类对象
synchronized(SynchronizeDemo.class){
}
锁任意的object实例对象,(公共资源)
String lock;
synchronized(lock){
}
6. 死锁
两个(或多个)线程各自占有一些共享资源,并且互相等待对方线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个线程使用
- 请求与保持条件:一个线程因请求资源和阻塞时,对已获得的资源保持不放
- 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
7. ReentrantLock
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock
类实现了Lock,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 =new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
private int ticket = 10;
//获取锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
//加锁
lock.lock();
Thread.sleep(1000);
while (ticket>=0){
System.out.println(ticket--);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
}
8. synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)
- 优先使用顺序
- Lock > 同步代码块(已经进入了方法体,分配了相应资源 )> 同步方法(在方法体之外)
9. 线程通信
并发协作模型“生产者 / 消费者模式”
必须在synchronized修饰的方法中
利用缓冲区解决(管程法):
生产者发现仓库满了,则自己wait()
,同时唤醒消费者notify()
,如果仓库空了则唤醒自己;
消费者发现仓库空了,则自己wait()
,同时唤醒生产者notify()
,如果仓库满了则唤醒自己;
/**
* 生产者消费者问题,创建缓冲区,管程法
* */
public class TestPC {
public static void main(String[] args) {
SynContainer container=new SynContainer();
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
new Thread(producer).start();
new Thread(consumer).start();
}
}
//生产者
class Producer implements Runnable{
//拿到缓冲区;
SynContainer container;
Producer(SynContainer container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i <=100 ; i++) {
//传入第i个产品
try {
//循环放入产品,并把id传进产品
container.push(new Product(i));
System.out.println("生产了第"+i+"个产品");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
class Consumer implements Runnable{
//拿到缓冲区;
SynContainer container;
Consumer(SynContainer container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i <=100 ; i++) {
//获取第i个产品
try {
//获取产品类的id
System.out.println("消费了第"+container.take().id+"个产品");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//产品
class Product{
//产品id
int id;
public Product(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer{
//创建容器
Product product ;
Product[] products =new Product[100];
int count=0;
//生产者放入产品
public synchronized void push(Product product) throws InterruptedException {
//如果容器满了,通知消费者拿取产品
if(count==products.length){
//通知消费者拿取产品,生产者等待
this.wait();
}
//如果容器未满,生产产品放入容器
products[count]=product;
count++;
this.notify();
}
//消费者拿取产品
public synchronized Product take() throws InterruptedException {
//如果容器没产品,通知生产者生产
if (count==0){
//消费者等待,生产者生产
this.wait();
}
//如果有产品,拿取产品
this.notify();
count--;
Product product=products[count];
return product;
}
}
信号灯法: 创建一个布尔类型的标志位flag,如果为true,就让他等待、如果为false,就让他去通知另外一个人、把两人衔接起来
10. 线程池
为什么使用: 频繁的创建和销毁使用量大的线程,对性能影响很大
好处:
- 提高响应速度,减少了创建新线程的时间
- 降低资源消耗,重复利用线程池中的线程,不需要每次创建
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
10.1 使用线程池
- JDK5.0提供了线程池相关的API:ExecutorService和Executors
- ExecutorService: 真正的线程池接口。常见子类ThreadPoolExecutor
void execute (Runnable command)
Reunnable的线程池执行方法,没有返回值<T> Future<T> submit (Callable<T> task)
Callable接口的线程void shutdown()
关闭线程池
- **Executors:**工具类、线程池的工厂类,用于创建并返回不同类型的线程池