一 .什么是多线程
1.1 进程
window上任务管理器中显示的都是进程;
定义:程序的一次执行,资源分配和调度的独立单位,但是线程实现多处理非常耗费CPU资源,所以引入线程代替进程的部分调度功能,作为调度和分派的基本单位。
同一个进程内又可以执行多个任务,每个任务都可以看为一个线程
总结:
- 进程是资源分配的基本单位;
- 线程是资源调度的基本单位;执行、
就绪、阻塞;派生、阻塞、激活、调度、结束; - 多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈
image.png
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
线程状态.png
多线程的存在,不是提⾼程序的执⾏速度。其实是为了提⾼应⽤程序的使⽤率,程序的
执⾏其实都是在抢CPU的资源, CPU的执⾏权。多个进程是在抢这个资源, ⽽其中的某⼀个进程如果执
⾏路径⽐较多,就会有更⾼的⼏率抢到CPU的执⾏权
1.2 并行域并发
多核多进程.png
1.3 java实现多线程
基于Thread类:
image.png
创建线程的两种方式,一种是继承Thread类并重写run方法,然后这个类实例.start启动线程;
另一种是实现Runnable或Callable接口重写run方法,new Thread(p).start()
image.png
package Threey;
/**
* @author : liulinzhi
* @date: 2020/09/17/19:34
* @description:
*/
public class ThreadDemo {
public static class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}
public static class MyThread extends Thread {
@Override
public void run() {
for (int x = 0; x < 200; x++) {
System.out.println(x);
}
}
}
public static void main(String[] args) {
// 创建两个线程对象
// MyThread my1 = new MyThread();
// MyThread my2 = new MyThread();
// my1.start();
// my2.start();
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
1.4 细节
run()和start()区别:
- run方法:封装被线程执行的代码,直接调用就是个普通方法;
- start方法:首先启动线程,然后由JVM调用run方法
JVM启动是多线程:因为至少启动main和垃圾回收线程
一般我们用实现Runnable接口:
- 避免单继承限制
- 将并发运行任务和运行机制解耦
二. Thread类解析
2.1 设置线程名
查看线程名:Thread.currentThread().getName();
下面我们查看源码,看默认的名字是怎么取得?
首先进入Thread()无参构造
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
可以看到默认名字是"Thread-" + nextThreadNum()
而
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
private static int threadInitNumber;
这个变量表示线程初始化得数量
也就是Thread-i
我们可以用实现Runnable接口得对象和自定义线程名字来创建线程
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
也可以用setName修改名字
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
if (threadStatus != 0) {
setNativeName(name);
}
}
2.2 守护线程
守护线程为其他线程服务,比如垃圾回收线程;
特点:
- 别的用户线程执行完毕,虚拟机会退出,守护线程也就被停止
- 也就是说没有服务对象就没必要继续运行了
1)线程启动前可以用setDaemon(boolean on)来设置当前线程为守护线程
2)守护线程不要访问共享资源,因为它随时会挂
3)守护线程中产生得新线程也是守护线程
2.3 优先级线程
优先级高==获取CPU时间片概率高,并不意味者必定先执行
java默认初始优先级是5,共有10个优先级
/**
* 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;
可以用setPriority
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
2.4 线程得生命周期
- sleep方法:让线程进入计时等待状态,时间到了进入就绪态而不是运行态
sleep.png -
yield方法:让别的线程限制性,但不是必定会让出CPU
yield.png - join方法:等待该线程执行完毕后才执行别的线程
三 .使用多线程需要注意的问题
3.1 使用多线程遇到的问题
- 线程安全问题:因为多个线程操作共享变量或一些组合操作很可能出问题;
- 性能问题:比如无脑添加同步锁,很可能会造成效率低,或是死锁问题
3.2 解决问题
- 不设置共享变量
- 使用final修饰
- 加锁
- JDK提供的线程安全类:比如AtomicLong满足原子性,ConcurrentHashMap等
3.3 原子性和可见性
3.3.1原子性(某个不可分割的操作)
比如自增操作:读取主存值,寄存器中+1,赋回变量——三步;就会产生原子性问题;
-
用AtomicXxx包解决:
AtomicXxx.png
3.3.2 可见性(修改对所有线程可见)
多线程环境下:当某个线程让变脸修改,其他线程都会知道,
- 解决:volatile,保证可见性和防止指令重排序
- 原理:缓存一致性思路:当CPU写数据时,如果发现操作的变量时共享变量,即其他线程的工作内存也存在该变量,于是会发信号通知其他CPU该变量的内存地址无效。当其他线程需要使用这个变量时,如内存地址失效,那么它们会在主存中重新读取该值。
- 不保证原子性:volatile保证了读写一致性。但是当线程2已经使用旧值完成了运算指令,且将要回写到内存时*,是不能保证原子性的。
image.png - 为什么普通变量不能保证可见性:
Java内存模型通常在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值,依赖主内存作为传递媒介的方式实现可见性的。无论是普通变量还是volatile变量都是如此,普通变量和volatile变量的唯一区别就是:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
四 .synchronized锁和lock锁
4.1 synchronized锁
- 互斥锁:一次只允许一个线程进入被锁代码块
- 每个对象都有一个内置锁,而synchronized就是用对象的内置锁来把代码块锁住
4.1.1 原理
主要分为同步方法和同步代码块:
两种.png
分别由monitorenter和monitorexit指令实现同步代码块;由ACC_SYNCHRONIZED实现同步方法:
同步代码块实现.png
4.1.2 使用
- 修饰普通方法(锁类对象)
- 修饰代码块(锁this),也可以自定义用哪个对象来锁
- 修饰静态方法(类锁).class
这里要区分一下类锁和对象锁:分别获取二者的线程不冲突
不冲突,不互斥.png
4.1.3 重入锁
- 比如父类有一个同步方法,子类也有一个同步方法:此时一个线程进入子类的同步方法,而这个子类同步方法中调用了父类的同步方法——显然实例对象的锁还没有解开,但调用父类的同步方法不需要一把锁,因为锁的持有者是线程,此线程已经由对象锁了,所以需要的时候直接开锁进去
4.1.4 释放锁的时机
- 方法(代码块)执行完毕后自动释放锁
- 当一个线程执行的代码出现异常时,锁自动释放
4.2 Lock锁
-
特性:
特性.png
synchoronized和lock如何选择?
JDK1.6之后对synchoronized做了各种优化,优化操作:适应⾃旋锁,锁消除,锁粗化,轻量级锁,偏向锁,所以二者性能差别不大,所以可以常用
4.3 公平锁:按照线程发出请求的顺序获取锁;
非公平锁:可以“插队”
- lock和synchronized默认是非公平锁;
4.4 总结
- synchronized好⽤,简单,性能不差
- 没有使⽤到Lock显式锁的特性就不要使⽤Lock锁了。
五 .AQS
JUC(java util concurrent)下的locks包中有
第二个就是常说的AQS抽象类.png
- 我们的ReentrantLock\ReentrantReadWriteLock都是基于AQS实现的;
5.1 特性:
- AQS其实就是⼀个可以给我们实现锁的框架
- 内部实现的关键是: 先进先出的队列、 state状态
- 定义了内部类ConditionObject
- 拥有两种线程模式
独占模式
共享模式 - 在LOCK包中的相关锁(常⽤的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建
- ⼀般我们叫AQS为同步器