01概念准备
1、程序:自己写的能跑起来的代码,程序是指令和数据的有序集合,是一个静态的概念,是写死的一段文字
2、进程 :程序的一次执行过程,是一个动态的概念,进程是资源分配的单位
3、线程:一个进程中包含多个线程,比如main线程、GC线程等等,线程是CPU调度和执行的单位。多个CPU即多核才能实现真正意义上的多线程。
4、线程就是独立的执行路径,互不干扰,比如main线程和GC线程
5、在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、GC线程
6、main()称之为主线程,是程序的入口
7、在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度的先后顺序由OS决定无法人为干预,但可以设置优先级
8、对同一份资源操作,存在资源抢夺问题,要加入并发控制
9、线程会带来额外的开销,如CPU调度时间、并发控制开销
10、每个线程在自己工作内存交互,内存控制不当会造成数据不一致
02线程创建
1、extends Thread
(1) 自定义类继承Thread,重写run()
(2)创建自定义类对象,调用start()开启线程
2、implements Runnable
(1) 自定义类实现Runnable,实现run()
(2)创建自定义类对象
(3)创建Thread对象作为代理类对象,并传入自定义类对象
(4)代理类对象调用start()开启线程
3、implements Callable
(1)实现Callable接口,需要call()返回值类型作为接口泛型
(2)重写call方法,需要抛出异常
(3)创建实现类对象
(4)创建执行服务,ExecutorService ser = Executors.newFixedThreadPool(线程数量);
(5)提交执行Future<call返回值类型> future = ser.submit(实现类对象);
(6)获取结果 future.get();
(7)关闭服务ser.shutdownNow();
三种方式优缺点
1、继承Thread类不建议使用,具有单继承局限性。
2、实现Runnable接口推荐使用,方便同一个自定义线程类被多个代理线程使用
3、静态代理模式,实现Runable接口创建自定义线程类,然后将其对象传入Thread代理类中完成实际操作。真实类和代理类都要实现同一个接口,在这里Thread也实现了Runnable接口,所以是代理模式
Lambda表达式 ()->
1、new Thread(()->{方法体}).start();
2、除了让人看不懂,貌似没啥用
线程状态
1、join():线程插队,插队时其他线程被阻塞,等插队线程先执行完,其他线程才能继续执行
2、yield():礼让,当前正在执行的线程暂停,但不阻塞,运行态->就绪态,然后和其他线程处在同一起跑线上也就是都在就绪态,然后再同时抢占CPU调度,主动礼让的线程不一定能礼让成功。
守护线程
1、线程分为用户线程和守护线程
2、虚拟机必须确保用户线程执行完毕main
3、虚拟机不用等待守护线程执行完毕,gc
4、如后台记录操作日志,监控内存,垃圾回收
线程同步
同一进程的多个线程共享一块存储空间,存在访问冲突问题,在访问时加入锁机制,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在下述问题:
- 一个线程持有锁会导致其他需要锁的线程挂起
- 在多线程竞争环境下,加锁,解锁会导致比较多的上下文切换和调度延时
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置
synchronized实现同步
https://blog.csdn.net/weixin_43689805/article/details/123198715?spm=1001.2014.3001.5501
Lock显式锁
接口java.util.concurrent.locks.Lock
实现类ReentrantLock可重入锁
- synchronized和Lock的对比
(1)Lock是显式锁(手开手关),synchronized是隐式锁,执行完代码块自动释放
(2)Lock只有代码块锁,synchronized有代码块锁和方法锁
(3)使用Lock锁,JVM将花费较少的时间来调度线程,性能更好
(4)优先顺序
Lock > 同步代码块 > 同步方法 - 使用代码
class A{
private final Lock = new ReentrantLock();
public void m(){
lock.lock();
try{
//需要保证线程安全的代码
}catch(){}
finally{
lock.unlock();
}
}
}
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,一直等待的情形。
-
某一个同步代码块拥有 “两个以上对象的锁”时,就可能发生“死锁问题”。
-
产生死锁的四个必要条件:
(1)互斥条件:一个资源只能被一个进程独占使用
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放,
(3)不剥夺条件:进程已获得的资源在未使用完之前,不能被其他进程强行剥夺
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系 -
只要破坏其中任意一个或多个条件,就可以避免死锁发生
线程协作—生产者消费者模式
生产者和消费者共享缓冲区这一资源:
对于生产者,没有生产产品之前,需要通知消费者等待,而生产了产品之后,有需要马上通知消费者消费
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
synchronized不能解决线程之间的通信问题
- 两种解决方案:
(1)管程法:在生产者和消费者之间设置缓冲区
(2)信号灯法:设置boolean标志位消费和生产。
package thread_synchronize;
public class bufferTest {
//在生产者消费者之间设置缓冲区,,,管程法
public static void main(String[] args) {
Buffer buffer = new Buffer();
new Producer(buffer).start();
new Consumer(buffer).start();
}
}
class Producer extends Thread{
Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
buffer.push(new Chicken(i));
System.out.println("正在生产第"+i+"只鸡");
}
}
}
class Consumer extends Thread{
Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费第"+buffer.pop().id+"只鸡");
}
}
}
class Buffer{
Chicken[] chickens = new Chicken[10];
int count; //容器计数器
//生产者放入产品
public synchronized void push(Chicken chicken){
if(count==chickens.length){
//生产者等待
try{
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满,需要放入产品
chickens[count] = chicken;
count++;
//可通知消费者消费
this.notifyAll();
}
//消费者取出产品
public synchronized Chicken pop(){
if(count==0){
//消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以取,那就取
count--;
Chicken chicken = chickens[count];
//可通知生产者生产,
this.notifyAll();
return chicken;
}
}
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
package thread_synchronize;
public class FlagTest {
//信号灯法解决生产者消费者问题,缓冲区只能放1只鸡情况
public static void main(String[] args) {
Flag flag = new Flag();
new Produce(flag).start();
new Consume(flag).start();
}
}
class Produce extends Thread{
Flag flag;
public Produce(Flag flag) {
this.flag = flag;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
flag.push("第"+i+"只鸡");
System.out.println("正在生产第"+i+"只鸡");
}
}
}
class Consume extends Thread{
Flag flag;
public Consume(Flag flag) {
this.flag = flag;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费第"+flag.pop()+"只鸡");
}
}
}
class Flag{
String chicken = new String();
boolean flag=false; //默认缓冲区wu鸡
//生产者放入产品
public synchronized void push(String chicken){
if(flag==true){
//生产者等待
try{
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满,需要放入产品
this.chicken=chicken;
flag=true;
//可通知消费者消费
this.notifyAll();
}
//消费者取出产品
public synchronized String pop(){
if(flag==false) {
//消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以取,那就取
flag=false;
//可通知生产者生产
this.notifyAll();
return chicken;
}
}
线程池
-
背景:经常创建和销毁使用量特别大的资源,比如并发情况下的线程,对性能影响很大
-
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建和销毁、实现重复利用,类似于共享单车
-
好处:
-
提高响应速度,减少了创建新线程的时间
-
降低资源消耗,重复利用线程池中线程,不需要每次新创建
-
便于线程管理
- corePoolSize :最多能放多少线程
- maximumPoolSize:最多同时跑线程数
- keepAliveTime:线程没有任务时最多保持多长时间销毁
-
线程池相关接口及实现类
(1)ExecutorService接口,实现类为ThreadPoolExecutor- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务/命令,有返回值,一般用来执行Callable
- void shutdown():关闭连接池
(2)Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
ExecutorService service=Executors.newFixedThreadPool(10);