目录
一、Java线程在代码中如何体现?
java.lang.Thread类(包括子类)的一个对象
二、如何在代码中创建线程
1.继承Thread类
继承Thread类,并重写run方法
public class MyFirstThreadClass extends Thread{
@Override
public void run() {
//这个方法下写的所有代码,如果正确创建线程的话,都会运行在新的线程执行流中
System.out.println("这是我的第一个线程");
}
}
实例化该类对象
public class Main {
public static void main(String[] args) {
MyFirstThreadClass t = new MyFirstThreadClass();
// t 指向一个创建出来的线程对象
}
}
2.实现Runnable接口
实现Runnable接口,并重写run方法
public class MyFirstTask implements Runnable{
@Override
public void run() {
System.out.println("这是我的第一个任务的第一句话");
}
}
实例化Runnable对象,利用该对象去构建Thread对象
public class Main {
public static void main(String[] args) {
MyFirstTask task = new MyFirstTask(); //创建了一个任务对象
Thread t = new Thread(task); //把 task 作为 Thread 的构造方法传入,此时语句就运行在行的线程中了
}
}
3.Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable线程,不能直接放入继承Thread的类
三、启动线程
当手中有一个Thread对象时,调用其start()方法。
注意:①一个已经调用过start()后不可以再调用;②千万不要调用成run();
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
1.如何理解t.start()做了什么?
把线程的状态从新建变成了就绪。线程被加入线程调度器的就绪队列中,等待被调度器选中分配CPU。
从子线程进入就绪态开始,子线程和主线程在低位上就完全平等了,此时哪个线程被选中分配CPU完全是听天由命。
2.为什么大概率先执行主线程?
t.start()是主线程语句。换言之,这条语句被执行了,说明主线程现在还在CPU上。而刚执行完t.start()就立马发生线程调度的概率不大,所以大概率先执行主线程。
四、常用函数说明
1.sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠。从线程的状态角度来看,就是让当前线程从“运行”→“阻塞”。
2.join()
join是Thread类的一个方法,其作用是:“等待该线程终止”。这里需要理解的就是该线程是指主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
为什么要用join()方法?
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
public class Join {
private static class B extends Thread{
//模拟B要做一个长时间任务
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
println("B拿到了钱");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
println("B把钱送给A");
}
private static void println(String msg){
Date date = new Date();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(format.format(date) + ":" + msg);
}
public static void main(String[] args) throws InterruptedException {
B b = new B();
b.start();
println("A吃饭但没钱");
//观察有join和没有join的区别
b.join();
println("A结账走人");
}
}
}
输出结果如下:
2022-07-22 18:17:00:A吃饭但没钱
2022-07-22 18:17:05:B拿到了钱
2022-07-22 18:17:10:B把钱送给A
2022-07-22 18:17:10:A结账走人
3.yield()
暂停当前正在执行的线程对象,并执行其他线程。从线程的状态角度来看,就是让当前线程从“运行”→“就绪”。随时可以继续被调度回CPU。
yield()主要用于执行一些耗时比较久的计算机任务,为了避免“卡顿”,时不时让出一些CPU资源给OS中的其他进程。
sleep()和yield()的区别:
sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到就绪态,所以执行yield()的线程有可能在进入到就绪态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
4.setPriority()和getPriority()
更改/查询线程的优先级
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
public class Priority {
public static void main(String[] args) {
Thread main = Thread.currentThread();
System.out.println(main.getPriority());
main.setPriority(Thread.MAX_PRIORITY);
System.out.println(main.getPriority());
main.setPriority(Thread.NORM_PRIORITY);
System.out.println(main.getPriority());
main.setPriority(Thread.MIN_PRIORITY);
System.out.println(main.getPriority());
}
}
setPriority()也只是给JVM建议,不可以强制让哪个线程优先调度。
5.interrupt()
不要以为它是中断某个线程!它只是向线程发送一个中断信号,告诉他要停止,实际上并不会影响线程运行。
线程处于休眠状态时(比如:sleep,join)意味着线程无法立马执行interrut()。此时,JVM的处理方式是以异常形式通知线程。当线程捕获到异常就知道有人让他停止了。
public class Interrupt {
static class B extends Thread{
@Override
public void run() {
while (true){
if(Thread.interrupted()){
//休息后有人要我停止
break;
}
for (int i = 0; i < 1000; i++) {
System.out.println("我正在工作哟");
}
if(Thread.interrupted()){
//休息前有人要我停止
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//休息的时候有人要我停止
break;
//大概率是在这里停下,因为工作只用毫秒即可完成,但是休息可以休息一秒
}
}
System.out.println("我不工作咯");
}
}
public static void main(String[] args) {
B b = new B();
b.start();
Scanner s = new Scanner(System.in);
s.nextLine(); //主线程在此阻塞,直到有用户输入
b.interrupt();
}
}
6.currentThread()
返回一个Thread引用,指向一个线程对象;在那个线程中调用该方法就返回哪个对象。
五、线程安全(重点)
1.什么是线程不安全
单看代码没有问题,但是结果不符合预期就称为线程不安全。
2.线程不安全的原因
-
站在开发者的角度
多个线程之间操作了同一块数据,且至少有一个线程正在修改这块共享数据。 -
站在系统的角度
①原子性被破坏(最常见的原因)
程序员的预期中r++和r--是一个原子性操作,但实际执行起来保证不了原子性;COUNT越大,线程执行需要跨时间片的概率越大(碰到线程调度的概率越大)导致出错率越大。
public class NoSafe {
// 定义一个共享的数据 —— 静态属性的方式来体现
static int r = 0;
// 定义加减的次数
// COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
static final int COUNT = 100000;
// 定义两个线程,分别对 r 进行 加法 + 减法操作
static class Add extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r++;
}
}
}
static class Sub extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Add add = new Add();
add.start();
Sub sub = new Sub();
sub.start();
add.join();
sub.join();
// 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
// 所以,结果应该是 0
System.out.println(r);
}
}
常见的违反原子性的场景:
①read-write场景
- i++;
- arr[size] = e;size++;
②check-update场景
if(a == 10){
a = 20;
}
②内存可见性
线程所有的数据操作(读/写)必须:①从主存加载到工作内存;②在工作内存中进行处理;③处理完毕后,再将数据同步回主存。
这就导致了内存可见性问题:一个线程对数据的操作,很可能导致其他线程无法感知。
理解:班级中有个账本(主存)班长出去买东西,班费减少,但未记录在账本上,只有班长脑子里(工作内存)知道这件事儿,同时学委出去采购,就会导致超支。
③重排序
我们写的程序往往经过了中间很多环节优化的结果,并不保证最终执行的和我们写的一样。作为应用开发,是无法得知做了什么优化的。所谓重排序就是指执行的指令和书写的指令不一样。
六、锁机制
1.synchronize锁
-
修饰方法(普通,静态)
-
同步代码块
public class UseSync {
// sync 修饰普通方法
public synchronized int add() {
return 0;
}
//等价于
public int add_2(){
synchronized (this){
return 0;
}
}
// sync 修饰静态方法
private synchronized static int sub() {
return 0;
}
//等价于
private static int sub_2(){
synchronized (UseSync.class){
return 0;
}
}
// sync 同步代码块
public void method() {
Object o = new Object();
synchronized (o) {
}
}
// sync 同步代码块
public void method2() {
synchronized (UseSync.class) {
}
}
}
锁理论上就是一段被多个线程共享的数据
sync(ref){ //对ref引用指向的对象加锁
//执行一些语句
} //解锁
多个线程都有加锁操作时,且申请的是同一把锁,就会造成加锁某代码解锁。临界区代码就会互斥进行。例:
public class Demo {
// 这个对象用来当锁对象
static Object lock = new Object();
static class MyThread1 extends Thread {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 1000; i++) {
System.out.println("我是张三");
}
}
for (int i = 0; i < 1000; i++) {
System.out.println("我是王五");
}
}
}
static class MyThread2 extends Thread {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 1000; i++) {
System.out.println("我是李四");
}
}
for (int i = 0; i < 1000; i++) {
System.out.println("我是赵六");
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread1();
t1.start();
Thread t2 = new MyThread2();
t2.start();
}
}
2.JUC包(重点)
java.util.concurrent.*;是现代java并发编程常用的包。
- java.util.concurrent.locks;下的Lock接口
- void lock(); 等同于sync
- void lockInterruptibly90; 加锁,但是允许被打断
- boolean tryLock(); 加锁失败返回fals
- boolean tryLock(long time, TimeUnit unit); 时间段内加锁,失败返回fals
- void unlock(); 解锁
使用方法:
Lock l = new ReentrantLock();
l.lock();
try{
//临界区代码
}finally{
l.unlock(); //保证解锁
}
public class NoSafeWithLock {
// 定义一个共享的数据 —— 静态属性的方式来体现
static int r = 0;
// 定义加减的次数
// COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
static final int COUNT = 100_0000;
// 定义两个线程,分别对 r 进行 加法 + 减法操作
// r++ 和 r-- 互斥
static class Add extends Thread {
private Lock o;
Add(Lock o) {
this.o = o;
}
@Override
public void run() {
o.lock();
try {
for (int i = 0; i < COUNT; i++) {
r++; // r++ 是原子的
}
} finally {
o.unlock();
}
}
}
static class Sub extends Thread {
private Lock o;
Sub(Lock o) {
this.o = o;
}
@Override
public void run() {
o.lock();
try {
for (int i = 0; i < COUNT; i++) {
r--; // r-- 是原子的
}
} finally {
o.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Add add = new Add(lock);
add.start();
Sub sub = new Sub(lock);
sub.start();
add.join();
sub.join();
// 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
// 所以,结果应该是 0
System.out.println(r);
}
}
七、volatile机制
用于修饰变量,线程要读写变量,每次从主存读,写入时要保证写会主存,用于保护内存可见性。
public class UseVolatile {
volatile static boolean quit = false;
//static boolean quit = false;
static class MyThread extends Thread{
@Override
public void run() {
long r = 0;
while (quit == false){
r++;
}
System.out.println(r);
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(5);
quit = true;
}
}
八、设计模式 (重点)
对一些解决通用问题而经常书写的代码片段的总结与归纳
1.饿汉模式
一开始就初始化
public class StarvingMode {
int a = 10;
// 是线程安全的
// static类加载的时候执行
// JVM 保证了类加载的过程是线程安全的
private static StarvingMode instance = new StarvingMode();
private static StarvingMode getInstance() {
return instance;
}
//privat构造方法保证无法初始化其他对象
private StarvingMode() {}
}
2.懒汉模式
用到了再初始化
public class LazyMode {
private volatile static LazyMode instance = null;
public static LazyMode getInstance() {
// 第一次调用这个方法时,说明我们应该实例化对象了
if (instance == null) {
// 只有 instance 还没有初始化时,才会走到这个分支
// 这里没有锁保护,所以理论上可以有很多线程同时走到这个分支
synchronized (LazyMode.class) {
// 通过上面的条件,
// 让争抢锁的动作只在 instance 实例化之前才可能发生,
// 实例化之后就不再可能发生。
// 加锁之后才能执行
// 第一个抢到锁的线程,看到的 instance 是 null
// 其他抢到锁的线程,看到的 instance 不是 null
// 保证了 instance 只会被实例化一次
if (instance == null) {
instance = new LazyMode(); // 只在第一次的时候执行
}
}
}
return instance;
}
private LazyMode() {}
}
九、wait()和notify()
wait() 和 notify() 方法属于Object类,Java中的对象都自带这俩方法。要使用他们必须先对对象进行sync加锁。
1.wait()
使当前线程等待,直到另一个线程为此对象调用notify()方法或notifyAll()方法
public class Demo1 {
static class MyThread extends Thread {
private Object o;
MyThread(Object o) {
this.o = o;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o) {
System.out.println("唤醒主线程");
o.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o) {
//放在锁里面,保证是主线程先执行
MyThread t = new MyThread(o);
t.start();
o.wait();
//1.释放o锁
//2.等待
//3.再次加锁
System.out.println("唤醒后到达");
}
}
}
2.notify()
- 随机唤醒
public class Demo3 {
static Object o = new Object();
static class MyThread extends Thread {
@Override
public void run() {
synchronized (o) {
try {
o.wait();
System.out.println(getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
MyThread t = new MyThread();
t.start();
}
// 保证了子线程们先 wait,主线程就先休眠一会儿
TimeUnit.SECONDS.sleep(5);
synchronized (o) {
o.notify();
// o.notifyAll();
}
}
}
- wait-notify是没有状态保存的,换言之,先notify后wait就会导致wait感知不到之前的notify,就会永远等待下去。
public class Demo4 {
static Object o = new Object();
static class MyThread extends Thread {
@Override
public void run() {
synchronized (o) {
System.out.println("notify");
o.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
synchronized (o) {
System.out.println("wait");
o.wait();
}
System.out.println("解锁成功");
}
}