多线程
一、线程、进程、并发、并行都是什么?
在开始多线程学习之前,说先需要了解一下线程、进程、并发、并行分别是什么呢?
线程 进程
专业的说法是:进程是资源分配的最小单位,线程是CPU调度的最小单位。
没懂?那做个简单的比喻:进程=火车,线程=车厢 。这下懂了吧,一个进程可以调用多个线程同时执行。
并发 并行
- 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力
二、多线程的创建方式
1.继承于Thread类
- 首先创建一个子类继承于Thread类
- 重写run()方法
- 创建Thread子类的对象
- 调用start()方法
四步解决!比把大象装冰箱只多一步,是不是很简单?
来一个窗口卖票小例子!
package com.qzh.studySE;
/**
* 创建三个窗口卖票,总票数为100张,使用继承自Thread方式
* 用静态变量保证三个线程的数据独一份
* 存在线程的安全问题,有待解决
* */
public class Main{
public static void main(String[] args){
window t1 = new window();
window t2 = new window();
window t3 = new window();
t1.setName("售票口--A");
t2.setName("售票口--B");
t3.setName("售票口--C");
t1.start();
t2.start();
t3.start();
}
}
class window extends Thread{
//将其加载在类的静态区,所有线程共享该静态变量
private static int ticket = 100;
@Override
public void run() {
while(true){
if(ticket>0){
System.out.println(
getName()+"当前售出第"+ticket+"张票");
ticket--;
}else{
break;
}
}
}
}
运行结果:
显然这种方法有点呆,第100张票被三个人都抢了,这就需要用锁来解决这个问题。(下节课咱们复习“锁”)。但咱们通过继承Thread重写run()方法创建的三个线程都执行成功啦!
2. 实现Runable接口
1.创建一个Runnable接口的实现类
2.在实现类中重写Runnable接口的run方法
3.创建一个Runnable接口实现类的对象
4.创建Thread类对象,构造方法中传参为:Runnable接口实现类的对象
5.调用Thread类中的start方法,启动多线程
实现Runnable接口创建多线程与继承Thread类相比优点:
- 避免了单继承的缺陷,如果一个类继承了Thread类就不能继承其他类,而实现Runnable接口后还可以继承别的类、或者实现别的接口。
- 实现Runnable接口降低了线程对象和线程任务的耦合性,把线程对象和线程任务Thread分离。
- 实现Runnable接口将线程对象单独封装,更体现面向对象思想。
public class Main {
public static void main(String[] args) {
Windows win1 = new Windows();
Windows win2 = new Windows();
new Thread(win1).start();
new Thread(win2).start();
}
}
class Windows implements Runnable{
@Override
public void run() {
for(int i = 0;i < 10;i++){
System.out.println(Thread.currentThread().getName()+" --- "+i);
}
}
}
结果如下:
三、Thread的几种方法及一些常识
1.stop()和interrupt()
区别:他们都是停止当前线程,stop方法是立即停止,不安全。interrupt则是标记这个线程可以结束了,但是怎么结束,可以在run()中通过if (isInterrupted)控制关闭建立的链接等操作后再退出,较为安全。
2.wait()
wait()是Object里面的方法,而不是Thread里面的,这一点很容易搞错。它的作用是将当前线程置于预执行队列,并在wait()所在的代码处停止,等待唤醒通知。wait()方法调用后会释放出锁,线程与其他线程竞争重新获取锁。
3.notify()
使停止的线程继续执行,调用notify()方法后,会通知那些等待当前线程对象锁的线程,并使它们重新获取该线程的对象锁,如果等待线程比较多的时候,则有线程规划器随机挑选出一个呈wait状态的线程。
notify()调用之后不会立即释放锁,而是当执行notify()的线程执行完成,即退出同步代码块或同步方法时,才会释放对象锁。
4.suspend()和resume()
suspend是暂停当前线程,resume是恢复暂停的线程。
5.synchronized方法
被 synchronized标识的方法,拿到实例的对象锁的线程才可以执行,保证同一时刻每一个类实例至多只有一个synchronized方法执行,其余线程等待此线程执行结束释放锁后方可执行。
6.synchronized代码块
将可能存在线程不安全的代码用synchronized代码块包起来,与synchronized方法不同的是,synchronized方法的锁是this(静态同步函数的锁是.class对象),而synchronized块使用的锁可以是任意对象(包括一个string字符串)。
7.线程的优先级
线程优先级具有继承性,比如a线程启动b线程,b线程与a优先级是一样的。
8.原子操作:不可被中断的一个或一系列操作
Java中原子操作更新基本类型,Atomic包提供了哪几个类?
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整形
AtomicLong:原子更新长整形
9.volatile关键字
区分工作内存和主内存:Thread的工作内存和主内存之间存在一个中间层cache,cache负责和主内存进行数据交换。假设我们改变了主内存中某个数据,如果子线程非常消耗CPU(比如 i++),cache就不会主动和主内存的共享变量同步,但如果很慢(I/O),cache才会将主内存中的共享变量更新到工作内存中。
volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性“。
对volatile定义如下,Java允许线程访问共享变量,为了保证共享变量能准确和一致的更新,线程应该确保排它锁单独获得这个变量。
如果一个字段被声明为volatile,Java线程内存模型所有线程看到这个变量的值是一致的(直接去主内存去拿)
四、死锁
1.概念
两个或多个线程由于资源竞争或者相互通信造成一种互相阻塞的状态,若无外力作用,他们都将无法继续推进下去。
如线程1拿到锁A正在执行同步方法,此时要获取锁B,而线程1拿到锁B正在执行同步方法,此时又要获取锁A,两个线程各自持有自己的锁,想要获取对方的锁,导致双方都进入了阻塞状态,这就是死锁。
2.代码示例
public static class QRunnable implements Runnable{
private Object LockA = new Object();
private Object LockB = new Object();
@Override
public void run() {
if(Thread.currentThread().getName().equals("A")){
runA();
}
else {
runB();
}
}
private void runA() {
synchronized (LockA){
System.out.println("我是A,我拿到了LockA,现在想要获取LockB");
synchronized (LockB){
System.out.println("我是A,执行结束");
}
}
}
private void runB() {
synchronized (LockB){
System.out.println("我是B,我拿到了LockB,现在想要获取LockA");
synchronized (LockA){
System.out.println("我是B,执行结束");
}
}
}
public static void main(String[] args) {
QRunnable r = new QRunnable();
Thread t1 = new Thread(r,"A");
Thread t2 = new Thread(r,"B");
t1.start();
t2.start();
}
}
运行结果:可见产生了死锁,相互阻塞,导致程序无法运行结束。
3.怎么查看死锁?
在terminal命令行输入jps -l
查看正在运行的进程
11008 jdk.jcmd/sun.tools.jps.Jps
13808 com.qzh.javase_test.Test1$QRunnable
1176 org.jetbrains.idea.maven.server.RemoteMavenServer
4184
12860 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
16204 org.jetbrains.jps.cmdline.Launcher
可以看到id为13808的就是我们运行的进程
然后输入jstack -l 13808
结果是“Found 1 deadlock.”和死锁具体信息。
4.死锁的四个必要条件
如果在一个系统中以下四个条件同时成立,那么就能引起死锁:
- 互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
- 占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
- 非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
- 循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
我们强调所有四个条件必须同时成立才会出现死锁。循环等待条件意味着占有并等待条件,这样四个条件并不完全独立。
5.怎么预先判断是否将会产生死锁?
通过称为系统资源分配图的有向图可以更精确地描述死锁。该图包括一个节点集合 V 和一个边集合 E。节点集合 V 可分成两种类型:P={P1,p2,…,Pn}(系统所有活动进程的集合)和 R={R1,R2,…,Rm}(系统所有资源类型的集合)。
结论:如果资源分配图没有环,那么系统就不处于死锁状态。如果有环,那么系统可能会也可能不会处于死锁状态。在处理死锁问题时,这点是很重要的。
6.避免死锁的方法
1.破坏“请求和保持”条件
想办法,让进程不要那么贪心,自己已经有了资源就不要去竞争那些不可抢占的资源。比如,让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。
2.破坏“不可抢占”条件
允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢到。
3.破坏“循环等待”条件
将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。
五、线程通信
例子1:创建一个List,线程A每秒钟将其中添加一个元素,当List长度是2的倍数时,线程B打印List中的元素。
public class Test1 {
private static Object lock = new Object();
private static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
Thread threadA = new Thread() {
@Override
public void run() {
Random r = new Random();
synchronized (lock) {
while (true) {
list.add(r.nextInt());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() != 0 && list.size() % 2 == 0) {
lock.notify();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
Thread threadB = new Thread() {
@Override
public void run() {
synchronized (lock) {
while (true) {
if (list.size() != 0 && list.size() % 2 == 0) {
System.out.println(list.toString());
}
lock.notify();//将A列入即将唤醒的队列
try {
lock.wait();//自己wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
threadA.start();
threadB.start();
}
}
例子2:2个厨师,每秒可做一个馒头,但馒头数大于10就不做了。三个消费者,一次可以吃1-5个馒头,馒头数量不够就通知厨师继续生产,厨师每生产一个就通知消费者吃。
public class Test1 {
private static Object lock = new Object();
private static LinkedList<String> list = new LinkedList<>();
public static void main(String[] args) {
Runnable process = new Runnable() {
@Override
public void run() {
while (true) {
synchronized (lock) {
if (list.size() >= 10) {
System.out.println("当前馒头数共有:" + list.size());
lock.notifyAll();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
list.add("馒头");
System.out.println(Thread.currentThread().getName() + "升产了一个馒头,现在有" + list.size());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
Runnable customer = new Runnable() {
@Override
public void run() {
while (true) {
synchronized (lock) {
Random r = new Random();
int eatnum = Math.abs(r.nextInt())%5 + 1;
if(list.size() < eatnum){
System.out.println(Thread.currentThread().getName() + "不够了!想吃"+eatnum+"个馒头,现在有" + list.size());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notifyAll();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else{
for(int i = 0;i < eatnum;i++){
list.pop();
}
System.out.println(Thread.currentThread().getName() + "吃了"+eatnum+"个馒头,现在有" + list.size());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notifyAll();
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
Thread t1 = new Thread(process,"厨师A");
Thread t2 = new Thread(process,"厨师B");
Thread t3 = new Thread(customer,"消费者A");
Thread t4 = new Thread(customer,"消费者B");
Thread t5 = new Thread(customer,"消费者C");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
3.利用管道可以线程间通信:
PipeInputStream、PipeOutStream、PipeWriter、PipeReader.
通过管道输入流和输出流的connect()方法建立通信。
六、锁的种类
1.公平锁、非公平锁的区别:
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。【synchronized是非公平锁】
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
2.乐观锁、悲观锁的区别:
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,一般会使用版本号机制或CAS算法实现。。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。