多个线程组成了一个进程,线程好比是一跟光纤中的一个玻璃丝,进程是整根光纤。
一个进程中的线程共享这个进程中的资源(内存、硬盘)
单核CPU发展出现瓶颈,想要再提高算力,只能增加CPU个数,并发编程就是利用多核CPU的绝佳方式.
使用进程也可以实现并发编程,只是进程重量大,创建销毁消耗资源多, 所以更好的方式是使用线程进行并发编程.
Java中线程是对于操作系统线程的封装和抽象.
public class Main {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread1.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t1 = new Thread(r1);
Runnable r2 = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread2.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
运行结果:
hello thread1.
hello thread2.
hello thread1.
hello thread2.
hello thread1.
hello thread2.
hello thread2.
hello thread1.
hello thread1.
hello thread2.
class MyThread extends Thread{
public void run() {
System.out.println("继承Thread得到");
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}
运行结果:
继承Thread得到
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口得到");
}
}
public class Demo2 {
public static void main(String[] args) {
MyRunnable r1 = new MyRunnable();
Thread t1 = new Thread(r1);
t1.start();
}
}
运行结果:
实现Runnable接口得到
继承Thread和实现Runnable接口的this指代的对象不同, 前者直接指代这个线程, 后者指代接口, 想要指代线程需要使用Thread.currentThread().
public class Demo3 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用Thread匿名内部类,直接传参new Runnable接口得到");
}
});
Thread t2 = new Thread() {
public void run() {
System.out.println("使用Thread匿名内部类,直接重写run方法得到");
}
};
t1.start();
t2.start();
}
}
运行结果:
使用Thread匿名内部类,直接传参new Runnable接口得到
使用Thread匿名内部类,直接重写run方法得到
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(()-> {
// 不需要重写run,lamda表达式就相当于是run方法
System.out.println("lamda表达式创建得到");
});
t.start();
}
}
运行结果:
lamda表达式创建得到
但是可能导致程序线程不安全, 需要合理加锁.
构造方法名 | 说明 |
---|---|
Thread() | 普通构造(仅分配空间) |
Thread(Runnable) | 根据所给的run()构造对象 |
Thread(String) | 为将构造出的线程进行命名 |
Thread(Runnable, String) | 根据run()创建对象并命名 |
命名主要是为了方便调试.
方法名 | 作用 |
---|---|
start() | 创建线程并运行 |
getId() | 返回线程的Id (这个Id不同于操作系统未进程分配的Id,也不是PCB中的Id,仅仅是JVM分配的Id) |
getName() | 返回线程名字 |
getPriority() | 返回优先级 |
getState() | 返回线程目前的状态(NEW, RUNNABLE, WAITING, TIMED_WAITING,BLOCKED,TERMINATED) |
isDaemon() | 判断是否为后台进程(后台进程不决定一个线程的存亡,只有前台进程才决定) |
isAlive() | 判断是否存活 |
isInterrupted() | 判断是否被中断 |
使用start()方法即可使其开始运行.
之前写过的run方法, 只是为这个线程规定要怎么做, 只有start方法才能启动线程.
start会调用系统api进行创建线程
run只是一个普通的方法,告诉线程的执行逻辑,不会创建线程
有两种方式:
线程A
和线程B
共享这个记号, 两个线程约定一个在其为true
时工作, 一个在其为false
时工作, 此时如果在A中对于这个记号进行更改, 那就能够使得B停止工作.public class Demo5 {
// 设置共同变量
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("t1");
// 在第一个线程执行完后暂停3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 3秒后,设置共同变量为false
flag = false;
});
Thread t2 = new Thread(()->{
while (flag == true) {
System.out.println("t2");
}
});
t1.start();
// 在第二个线程执行前暂停2秒,让t1线程运行2秒
Thread.sleep(2000);
// 意味着t2只能执行1秒
t2.start();
}
}
运行结果:
t1
(等待3秒)
t2
t2
...
t2
t2
在3秒后,t1线程将共享变量修改为false, 所以t2被中断.
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (!Thread.interrupted()) {
System.out.println("t1尚未被中断");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("t1收到中断信号");
throw new RuntimeException(e);
}
break;
}
});
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
// 暂停2秒后进行中断
Thread.sleep(1);
t1.interrupt();
}
}
运行结果:
NEW
RUNNABLE
t1尚未被中断
t1收到中断信号
Exception in thread "Thread-0" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
at Demo6.lambda$main$0(Demo6.java:10)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Demo6.lambda$main$0(Demo6.java:7)
... 1 more
interrupted() 和 currentThread().isInterrupted() 截然不同:
方法名 | 说明 |
---|---|
interrupted() | 查看当前线程是否被中断, 清除标记为false |
currentThread().isInterrupted() | 查看当前线程是否中断,仅作判断, 不清除标记 |
演示:
public class Demo7 { public static void main(String[] args) { Thread t1 = new Thread(()->{ for (int i = 0; i < 10; i++) { //System.out.println(Thread.interrupted()); System.out.println(Thread.currentThread().isInterrupted()); } }); t1.start(); t1.interrupt(); } } 运行结果: true true true true true true true true true true //(这种方法不清除中断标记, 仅作判断)
public class Demo7 { public static void main(String[] args) { Thread t1 = new Thread(()->{ for (int i = 0; i < 10; i++) { System.out.println(Thread.interrupted()); } }); t1.start(); t1.interrupt(); } } 运行结果: true false false false false false false false false //(这种方法清除中断标记, 恢复为未被中断状态)
线程执行有先后顺序的时候**(线程A的执行需要依赖于线程B的执行结果), **那就需要使用join()方法, 这个方法能够保护当前的线程执行完毕后,其他线程才会去执行.
// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
public static int count = 0;// 1
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> {
System.out.println("t11");
for (int i = 0; i < 50000; i++) {
count++;
}
System.out.println("t11");
});
Thread t2 = new Thread(()-> {
System.out.println("t21");
for (int i = 0; i < 50000; i++) {
count++;
}
System.out.println("t21");
});
t1.start();
t1.join();
t2.start();
Thread.sleep(100);
System.out.println(count);
}
}
在都对count进行++五万次的操作中,可以不加锁,也能使得count得到预期值的方法就是让t2在t1执行结束之后才启动,这样两个线程都能完成自己的任务,得到预期count。
访问局部变量的过程:对象-> 方法->局部变量。访问局部变量就已经有了访问权限的设定了。由此加修饰符也成了摆设。
对应static来说,因为static只能修饰成员变量和成员方法,在局部变量中用static修饰,又不能直接被类调用。
使用 Thread.currentThread();
进行获取。
使用 Thread.sleep(long mills)
实现。
在线程内部需要捕获异常,在方法中使用需要抛出异常。
Thread t2 = new Thread(()-> {
System.out.println("t21");
for (int i = 0; i < 50000; i++) {
count++;
}
// 捕获异常
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t2");
// 抛出异常
public static void main(String[] args) throws InterruptedException {
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了
使用isAlive()方法可以观察线程是否存活。
使用yield()方法会使线程重新排队。
在两个线程进行对于同一个变量进行修改的时候会出现线程不安全的问题。
指令非原子性
即使是“++”这个操作,仅有一条语句,也是由3条微指令构成的:
从内存中读出count到寄存器
在寄存器中完成++
++后的值放进寄存器。
在多个线程的这三个操作如果相互穿插进行,那么就可能会读入”脏值“。
内存可见性
内存可见性,一个线程对共享变量值的修改,能够及时地被其他线程看到.
对于多次重复的读入同一个数据,编译器会对其进行优化,直接在寄存器中使用这份数据的拷贝值,不再从内存中进行读取,对这个变量的修改操作也都是在这个拷贝值身上完成,在这个线程使用完此变量后才会将最终值写进内存。
这种方式对于单线程来说是一种优化,简便了数据的读取操作,但是对于多线程来说,如果在线程A频繁修改变量count的同时,
线程B需要对count进行修改,那么就会读到“脏值”。
使用volatile
关键字,忽略编译器对其的优化。
synchronized 会将其所在的代码块进行加锁。
如果说一个代码块相当于是一间房,那么一个synchronized就相当于是给这个房间进行上锁,其他人想进去必须要等到里面的人把锁打开,两个人进行争夺房间的使用权的过程也称为“锁竞争”。
锁的作用就是让不同的线程拥有同一个对象的锁的时候,只有执行顺序靠前的线程能够正常运行,后面的线程需要等待前面的线程释放锁以后才能继续正常运行。
synchronized的底层是使用操作系统的mutex lock实现的.
synchronized工作过程本质上是通过获取一个安全的空间来进行保证操作原子性
的:
获得互斥锁
从主内存拷贝变量的最新副本到工作的内存 3.
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
public static final Object locker = new Object();
synchronized (locker) {
synchronized (locker) {
}
}
在对于一个对象上同一把锁两次的时候,理论上来说会产生“死锁”现象。
因为一个第二把锁所在的代码块执行的前提是第一把锁释放,但是第一把锁释放的条件是后序的代码块执行完,形成闭环,造成“死锁”。
1)互斥使用:同一把锁的不同线程同一时间只有一个能够运行
2)不可抢占:后面的线程只能等前面的将锁释放后才能运行
3)循环等待:在A阻塞等待B释放锁的时候,B在等待A释放锁
4)请求保持:一个线程尝试获取多把锁(线程A在已经被锁1加上的情况下获取一个已经被占用的锁2,那么锁1不会被释放)
1和2都是锁的基本特性,3和4是代码结构,当同时满足以上四点的时候才会发生死锁。
synchronized public void method1() {
}
synchronized public static void method1() {
}
//3
public void method2() {
synchronized (this) {
}
}
//4
public void method3() {
synchronized (Demo2.class) {
}
}
其中,3和4等价。
不安全的:ArrayList 、LinkedList、 HashMap、 TreeMap、 HashSet、 TreeSet、 StringBuilder
安全的:Vector (不推荐使用)、 HashTable (不推荐使用) 、ConcurrentHashMap、 StringBuffer
内存可见性,一个线程对共享变量值的修改,能够及时地被其他线程看到.
import java.util.Scanner;
// volatile的作用
public class Demo3 {
//public volatile static int isQuit = 0;
public static int isQuit = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> {
while (isQuit == 0) {
}
System.out.println("t1退出");
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(()-> {
System.out.println("请输入");
Scanner scanner = new Scanner(System.in);
isQuit = scanner.nextInt();
});
t2.start();
Thread.sleep(1000);
System.out.println(t1.getState());
}
}
由于编译器的优化, t2对于isQuit变量进行修改并不影响t1线程中看到的isQuit变量是0, 这就叫做内存不可见.
但是如果加上volatile, 那么编译器会保证内存的可见性, 放弃优化.(所以会将代码的运行效率降低)
volatile的工作过程:
如果是读取:
volatile虽然一次性将数据读取到工作内存, 待其写完后又放回主内存, 但是在写的过程中, 如果其他线程也对同一个变量进行写入, 这将是合法的, 并且存在线程安全问题.
// 线程安全问题
public class Main {
// 加上volatile并不能够得到预期的count值
public static volatile int count = 0;// 1
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
count++;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
count++;
}
}, "t2");
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}
}
结果:
75377