多线程
大一菜鸡的个人笔记,欢迎指点和交流。
多线程
原来只有一条路,车多了会拥挤,效率极低
多加几个车道,效率高,即多线程。
进程与线程
进程:在操作系统中运行的程序就是进程,比如QQ、游戏等。
一个进程可以有多个线程,比如在视频中可以同时听声音、看图像、看弹幕。
线程的特性
程序是一个静态的概念。而进程是执行程序的一次执行过程,是一个动态的概念。是系统分配资源的概念。
一个进程中可以包含多个线程,至少有一个线程(Main线程)。 线程是CPU调度和执行的单位。
很多的多线程是模拟出来的,即一个CPU在同一时间点,只能执行一段代码。因为切换的快所以有同时进行的错觉。真正的多线程是多核CPU。
线程的创建
继承Thread类(重点)
继承Thread类
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象 调用start()方法启动线程
start方法——同时进行
实际上主线程和副线程是同时执行的
run方法——先后进行
先调用run方法再执行主线程
不建议使用:避免OOP单继承局限性
实现Runnable接口(最重要)
实现Runnable接口
推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
实现Callable接口(了解)
-
实现Callable接口
-
重写call方法 需要返回一个值
-
创建执行服务
-
提交执行
-
获取结果
-
关闭服务
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * @author:zmc * @function: * @date: 2020/7/16 11:01 */ public class Callable implements java.util.concurrent.Callable { public static void main(String[] args) { //创建线程池(不正规的方法) ExecutorService ser = Executors.newFixedThreadPool(1); Callable t1 = new Callable(); //提交执行 Future r1 = ser.submit(t1); //获取结果 // Object o = r1.get(); //关闭服务 ser.shutdown(); for (int i = 0; i < 500; i++) { System.out.println("Main哈哈哈"+i); } } @Override public Object call() throws Exception { for (int i = 0; i < 500; i++) { System.out.println("Callable哈哈哈"+i); } return null; } }
callable和主线程是同时进行的
callable的好处
-
可以定义返回值
-
可以抛出异常
静态代理:线程的底部原理
真实对象和代理对象都要实现同一个接口
代理对象要代理真实角色
优点
- 代理对象可以做很多真实对象做不了的事情
- 真实对象可以专注做自己的事情
- 代理对象对真实对象增强
静态代理和多线程的关系
Thread相当于weddingcompany 都是代理 中间为真实对象(Runnable接口)调用了start方法
Lambda表达式:Java8新技术
λ希腊字母表排序第十一位的字母
理解Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在
函数式接口:只包含一个唯一一个抽象方法 可以用lambda表达式来创建对象
静态内部类
局部内部类
匿名内部类
Lambda表达式的使用:直接实现
有类型的写法
无类型的写法
无括号的写法(只能有一个参数)
去除花括号(只能有一行实现)
Lambda表达式的优点
- 避免匿名内部类定义过多
- 使代码更加简洁
- 只留下核心逻辑
线程状态
线程方法
停止线程
不推荐用stop() destroy()方法
推荐让线程自己停下来
建议使用标识变量模拟停止:
线程休眠
-
sleep(时间)指定当前线程阻塞的毫秒数
-
sleep存在异常InterrupteedException
-
sleep之后进入就绪状态
-
sleep可用于模拟网络延时,倒计时等
-
每个对象都有一个锁 sleep不会释放锁
倒计时
线程礼让
方法:Thread.yield();
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度 但不一定成功
线程插队
-
Join合并插队,其他线程阻塞。待此线程执行完成后,再执行其他线程。
观察线程状态
状态:
线程无法启动两次
线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,按照优先级来决定。
数字大的优先级高
先设置优先级,再启动
优先级高,分配的资源就多。不一定优先级高的先跑,但是概率会更高。
守护(daemon)线程
-
线程分为用户线程(main)和守护线程(如gc线程(garbage 垃圾线程))
-
虚拟机必须确保用户线程执行完毕
-
虚拟机不用等待守护线程执行完毕
-
守护线程作用:
-
- 后台记录操作日志
-
监控内存
- 垃圾回收等等
主线程结束,虚拟机停止还需要一段时间,然后守护线程被强行终止。
线程同步机制
多个线程操作同一个资源
并发:同一个对象被多个线程同时操作
概念
为了安全,在线程访问对象时加入锁(synchronized)机制,当一个线程获得一个对象的排他锁,独占资源,其他线程必须等待此线程使用完毕后释放锁。
还有以下问题:
- 一个线程持有锁,会导致其他所有需要此锁的线程挂起。
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会引起优先级倒置,引起性能问题。
线程不安全
线程同步->使线程变得安全
数据对象:通过private关键字来保证数据对象只能被方法访问
方法:使用synchronized关键字 包括两种用法:synchronized方法和synchronized块
synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
缺陷
若将一个大的方法申明为synchronized 将会影响效率
实际上方法内部不是每一处都需要上锁,此时使用同步方法会导致性能浪费。
synchronized方法
synchronized代码块
同步块需要监视器
死锁
多个线程持有对方需要的资源,形成了僵持。
产生死锁的四个必要条件
-
一个资源每次只能被一个进程使用
-
一个进程因请求资源而阻塞时,对已获得的资源保持持有状态
-
进程已获得的资源,在未使用完前,不能强行剥夺
-
若干进程之前形成一种头尾相接的循环等待资源关系
ReentrantLock(可重复锁)
可显式地上锁和解锁
Synchronized与ReentrantLock的对比
Lock是显式锁(手动上锁和解锁,一定要记得解锁),synchronized是隐式锁,出了作用域自动释放。
Lock只有代码块锁,synchronized有代码块锁和方法锁。
使用Lock锁,JVM将花费较少的时间来调度线程,性能较synchronized更好。并且有更好的拓展性(提供更多的子类)
优先使用顺序
Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
线程协作
消费者问题
解决方案1 管程法——利用缓存区
/**
* @author:zmc
* @function:生产者消费者模型 缓冲区方案————管程法
* @date: 2020/7/16 13:29
*/
public class PC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("生产了"+(i+1)+"只鸡");
container.push(new Chicken(i));
}
}
}
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("消费了"+(container.pop(new Chicken(i)).id+1)+"只鸡");
}
}
}
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer{
Chicken[] chickens = new Chicken[10];
int count = 0;
public synchronized void push(Chicken chicken){
if(count == chickens.length) {
//满了 通知消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count++] = chicken;
this.notifyAll();
}
public synchronized Chicken pop(Chicken chicken){
if(count == 0) {
//满了 通知消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count--] = chicken;
this.notifyAll();
return chicken;
}
}
解决方案2 信号灯法——
/**
* @author:zmc
* @function: 信号灯法
* @date: 2020/7/16 13:55
*/
public class PC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watch(tv).start();
}
static class Player extends Thread{
TV tv = new TV();
public Player(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if(i%2==0){
this.tv.play("正在播放快乐大本营");
}else {
this.tv.play("正在播放肥皂剧");
}
}
}
}
static class Watch extends Thread{
TV tv = new TV();
public Watch(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
public static class TV{
String voice;
boolean flag = true;
public synchronized void play(String voice) {
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了"+voice);
//通知观众观看
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
public synchronized void watch() {
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了"+voice);
//通知演员观看
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
}
}
线程池
背景
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
原理
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,实现线程的高效利用。
优点
提高响应速度(减少了创建新进程的时间)
降低资源损耗(重复利用线程池中线程,不必每次都创建)
便于线程管理
使用工具类,阿里规范要求通过ThreadPoolExecutor方法手动创建
线程组
为了方便管理一批线程,我们使用ThreadGroup来表示线程组。(A线程创建了B线程,B线程就是属于A线程的线程组的)
核心信息
- 名称
- 优先级
- 守护与否
- 父线程组
- 子线程组
线程组内部属性包括:自己的名字,父线程组,使用数组记录自己下面有哪些线程组
线程组相关的方法
ThreadGroup(String name) 创建名为name的新线程组
ThreadGroup(ThreadGroup parent,String name) 创建父线程组为parent的线程组
创建系统线程组
获取线程组名称
线程组优先级
指的是一个线程组中所有线程最大允许的线程的优先级,实际上线程中不一定有达到这个优先级的。
守护
默认值与父线程组相同
父线程组
子线程组
线程组内可以有线程组,层层嵌套形成树状结构。
创建线程组时,借助parent.add(this)方法,把自己添加到父线程组的记录数组groups[]。
对于任何一个线程,都有一个线程组,如果没有设置,就会把当前线程的线程组作为线程组。
每个线程组都知道自己有多少个线程,哪些线程;每个线程组也都知道自己有多少个线程组,哪些线程组。
子线程组相关方法
activeCount()获取线程组和子线程组所有活动线程个数的一个估计数
activeGroupCount()获取子线程组的个数的估计数
interrupt()中断线程组中的所有线程
线程组的销毁
destroy()
- 会进行权限检验
- 线程组必须为空——线程组的所有线程都已停止执行
- 会把子线程组递归销毁,并且也会进行子线程组的权限检验和判空
权限检验checkAccess
checkAccess()
借助安全管理器进行权限的检验的封装
确保当前运行的线程是否有权修改此线程组
线程相关类
ThreadLocal类
ThreadLocalVariable(线程局部变量),为每一个使用该变量的线程提供一个变量值的副本,使得每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量一样。
方法
T get():返回此线程局部变量中当前线程副本中的值
void remove() :删除此线程局部变量中当前线程的值
void set(T value):设置此线程局部变量中当前线程副本中的值
用途
ThreadLocal和其他同步机制一样,都是为了解决多线程中对同一变量的访问冲突,比起上锁,ThreadLocal从另一个角度来解决多线程的并发访问,将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都有自己的资源副本。在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存。
通常需要线程通信使用同步机制,仅仅隔离共享冲突则使用ThreadLocal。
包装线程不安全的集合类
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。
如果要保证线程安全,可以使用Collections提供的类方法把这些集合包装成线程安全的集合。
方法
//诸如此类 获取包装好的线程安全类
HashMap map = Collections.synchronizedMap(new HashMap());
线程安全的集合类
JUC包提供了大量的集合类,此处我会在JUC笔记记录。
本文地址:https://blog.csdn.net/Rush6666/article/details/107391392
上一篇: 买个锤子工作站
下一篇: 闺蜜冬天基本每周啊两次