Java SE 学习笔记-Day 07
基础概念
- 进程是系统分配资源的最小单位。
- 线程是实际执行的最小单位,一个进程里至少有一个线程。
- main()称为主线程,为系统的入口,用于执行整个程序。
- 各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。
两个关键字
transient
:被修饰了的变量在序列化时,不会被序列化。
volatile
:被修饰了的变量被一个线程修改(即将工作内存的数据写回主内存)时,会立即被多个线程感知,实现轻量级的线程同步方式。
线程状态
广义上的五个线程状态:
新生状态:被创建出来,Thread t = new Thread()
就绪状态:当调用了start方法,线程就就绪了
运行状态:CPU调度运行该线程
阻塞状态:线程需要等待输入或被sleep,wait等阻塞
死亡状态:线程中断或结束
Java里的六个线程状态
- NEW : 尚未启动的线程
- RUNNABLE : 在Java中运行的线程(就绪态和运行态 二合一)
- BLOCKED:被阻塞
- WAITTING:正在等待另一个线程执行特定动作的线程
- TIMED_WATTING:正在等待另一个线程执行动作达到指定等待时间(如调用sleep)的线程
- TERMINATED:已退出的线程
可以通过Thread.State
来观察线程状态,用Stare
对象接受线程对象调用getState
的返回值。
创建线程的两种方法
通过Thread类
- 创建继承了Thread的类
- 重写run方法
- 在main方法里,实例化自定义线程类,调用
start()
方法。- 若直接调用run()方法,相当于只是一个线程里面在调用方法而已。
通过Runnable类(推荐)
-
实现Runnable接口
-
重写run方法
-
在main方法里,实例化自定义线程类,将对象传入一个Thread类的构造函数,通过Thread类对象调用
start()
方法。- 若直接调用run()方法,相当于只是一个线程里面在调用方法而已。
实现Runnable接口更灵活,避免了Java单继承的局限,方便一个对象被多个线程使用。而一个Thread对象就代表一个对象,只能调用一次start方法,要建立新的线程,只能重新new 一个对象。而将一个Runnable对象传入多个Thread线程,即可共享一个对象。
多线程实例 - 龟兔赛跑
public class Race implements Runnable{
// 通过winner 来实现两个线程之间判断对方是否已到达终点
String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if(Thread.currentThread().getName().equals("兔子") && i == 50)
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (gameOver(i))
break;
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
}
}
boolean gameOver(int step){
if(winner != null) {
return true;
}
else{
if( step >= 100){
winner = Thread.currentThread().getName();
System.out.println(winner+"赢得了比赛");
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"乌龟").start();
new Thread(race,"兔子").start();
}
}
Callable接口(了解)
好处:可以定义并获得返回值,且可以抛出异常
-
实现Callable接口,需要传入返回类型,如
Callable<Boolean>
-
重载call方法
-
实例化对象
-
创建执行服务,指定线程数量
ExecutorService ser = Executor.newFixedThreadPool();
-
提交执行,需要接受返回值
-
获取结果
-
关闭服务
静态代理模式
- 代理对象和真实对象实现同一个接口
- 通过代理对象来执行一个方法,就可以在真实对象的基础上做很多其他事
自定义实现了Runnable接口类,实例化对象,就是一个真实对象
同时Thread类也实现了Runnable接口,我们将自定义类的对象传入Thread类的对象,Thread类就充当了一个代理对象,可以做自定义类里面未实现的工作。
Lamda表达式
避免匿名内部类定义过多,使代码更简洁。
Functional Interface(函数式接口):只包含一个抽象方法的接口,如Runnable
public interface Runnalbe{
public abstract void run();
}
- lamda表达式只能用于函数式接口
- lamda表达式重写的方法只有一行代码时,可以简化为一行,否则需要用花括号包裹
- 由于在接口的方法里已经定义了参数类型,所以lamda表达式中只需要写出形参名称即可。单个参数时,可以不用写
()
,多个参数时,则需要用()
,并用逗号隔开。
interface Love{
int love (int a,int b);
}
public static void main(String[] args) {
// 相当于实现了一个匿名内部类,重写了它的方法
Love me = (a,b) -> {
int c = a + b;
return c;
};
int c = me.love(2,5);
System.out.println(c);
}
线程睡眠
通过sleep方法,可以模拟网络延时和倒计时等功能
线程停止
现在已经不建议使用jdk内部的stop方法使线程停止,一般设置一个标志变量,然后重写stop方法,在这个方法里面,更改标志变量的值,使死循环终止。
线程礼让
某个运行态的线程主动转到就绪态,但不进入阻塞态,但能否礼让成功,要看CPU的调度策略,有可能还是让礼让的线程继续执行,不一定礼让成功。
Java里通过yield()
方法礼让。
Join
相当于插队,当一个线程主动调用该方法时,会阻塞其它所有线程,只执行该线程。
线程优先级
JVM有一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。线程优先级从1~10, 最高为10,最低为1,正常为5,一般都默认为5.
通过getPriority()
可获得优先级。
通过setPriority()
可设置优先级,主要要在启动前设置。
即使后启动的线程,但优先级高,也会先执行。
守护(Daemon)线程
线程分为用户线程和守护线程。JVM必须确保用户线程执行完毕,但不用等待守护线程执行完毕。守护线程,如垃圾回收等,进程存在时会一直存在,但当只剩下守护线程的时候,JVM就会退出。
可以在线程执行前,通过setDaemon
方法(传入true),设置线程为守护线程。
线程同步
线程同步就是一种等待机制,多个需要同时访问某个对象的线程,直接进入这个对象的等待池形成排队队列。
为了线程添加了锁机制(synchronized
),即拥有锁的线程才能访问资源,否则要一直等待拿到锁。
- 锁机制提高了安全性,但也会导致较多的上下文切换、优先级倒置等性能问题。
synchronized
使用synchronized
修饰某个方法,某个对象对应一把锁,每个synchronized
方法只有拥有这个对象的这把锁时,才能执行,一旦开始执行,就会独占该锁,直到该方法返回才释放锁,而后面阻塞的线程才能获得这个锁。
由于将某个方法都锁上, 效率比较低, 一般对数据只读的代码区没有必要锁上, 可以用synchronized
修饰代码区.
-
同步方法给调用它的对象(
this
)一把锁 -
而同步块可以设置同步监视器,监视任何指定对象(一般设置为会被增删查改的共享资源)
// 给对象加一把锁,也相当于独占这个对象 synchronized(共享资源的对象){ // 增删查找改代码块 }
JUC 是java下的一个包Java.util.concurrent,里面有线程安全的一些类
死锁
多个线程互相拥有着对方的资源(即锁),形成环路。
-
互斥条件:一个资源只能被一个进程使用
-
请求与保持条件:一个进程对拥有的资源保持不放,同时请求另一个资源
体现在代码里,即在
synchronized
代码块内,再嵌套一层,相当于持有的资源还未释放,又去获取新的资源 -
不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
Lock(锁)
java.util.concurrent.locks.Lock 接口提供了多个线程对共享资源访问的工具。
ReentrantLock类(可重入锁)实现了Lock接口,它拥有和synchronized相同的并发性和内存语义,可以显式地加锁,释放锁。
使用Lock锁,性能更高,更有效率。
class A{
private final ReentrantLock lock = new ReentrantLock();
public void m(){
// 加锁
lock.lock();
try{
// 需要保证线程同步的代码
}finally {
lock.unlock(); // 解锁
}
}
}
生产者消费者模型
假设仓库只能放一件产品,生产者将生产出来的产品放入仓库,消费者从仓库取走消费。若仓库没有产品,消费者不能进行消费,生产者可以将产品放入仓库后,停止生产,通知消费者取走,消费者取走后通知生产者又开始生产。即一个反映线程通信的模型。
- Object里面的
wait
方法会使线程进入等待状态,但会释放锁,sleep则不会。
synchronized
只能保护共享数据并发同步安全性,不能实现通信。为实现通信,有两种方式,管程 和 信号量
管程法:
建立一片缓冲区,使用wait
和notify
来进行通信。
信号量:
用一个标志位来通信
线程池
为避免线程经常创建、销毁,可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。提高响应速度,降低资源消耗,便于管理。
- ExecutorService类 用于管理、执行线程池中的线程。
- submit 方法执行继承Callable接口的类,且有返回值
- execute方法执行Runnable接口的类
- shutdown 关闭线程池
- Executors用于创建线程池,
Executors.newFixedThreadPool(threadNum)
,该方法返回指定线程数目的线程池。
// 1.创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 关闭
service.shutdown();