线程安全:多线程操作的共享变量能够返回预期的结果
进程-Process
- 程序的一次动态执行过程,占用特定地址空间
- 可包含多个线程
- 由cpu/data/code组成
- 资源分配单位
线程-Thread
- 进程内部的一个执行单元,程序中的一个单元顺序控制流程
- 调度和执行的单位
- 只有一个主线程,为系统入口,执行整个程序
- 线程的运行由调度器调度,调度器与操作系统有关,先后顺序不能人为控制
- 每个线程都有优先权,优先级高的线程优先于较低的线程执行(1-10)
线程的实现:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口 - 并发
/**
* Thread
*/
public class StartThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("搬砖第" + i + "天");
}
}
public static void main(String[] args) {
//创建
StartThread st = new StartThread();
//启动(系统执行)
st.start();
}
}
/**
* Runnable
*/
public class StartRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("搬砖第" + i + "天");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new StartRunnable());
thread.start();
}
}
/**
* Callable
*/
public class StartCallable implements Callable<String> {
@Override
public String call() throws Exception {
String tired = "搬砖累!!!";
for (int i = 0; i < 50; i++) {
System.out.println("搬砖第" + i + "天");
}
return tired;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建目标对象
StartCallable sc = new StartCallable();
//创建执行服务
ExecutorService service = Executors.newFixedThreadPool(1);
//提交执行
Future<String> result = service.submit(sc);
//获取结果
String r = result.get();
//关闭服务
service.shutdownNow();
}
}
线程状态:
-
新生状态(New)
new 创建线程后,线程处于新生状态,且已经分配了自己的内存空间,通过 start 方法进入就绪状态 -
就绪状态(Runnable)
处于就绪状态后,线程已经可以运行,但还没被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。当获得CPU后,线程就进入运行状态并自动调用自己的 run 方法。
导致线程进入就绪状态:- 新建线程:new 线程,调用 start()
- 阻塞线程:阻塞解除后,进入就绪状态
- 运行线程:调用 yield() 方法,直接进入就绪状态
- 运行线程:JVM将CPU资源从本线程切换到其他线程
-
运行状态(Running)
线程执行 run 方法时,直到调用其他方法而终止或等待资源而阻塞或完成任务死亡 -
阻塞状态(Blocked)
阻塞指暂停一个线程的执行以等待某个条件发生
阻塞原因:- 执行 sleep() 方法,使线程休眠,进入阻塞,时间到后,线程进入就绪状态
- 执行 wait() 方法,使当前线程进入阻塞状态;当使用 nofity() 方法唤醒线程后,进入就绪状态
- 线程运行时,受某个操作进入阻塞,如执行IO流操作,阻塞去除后,进入就绪状态
- join() 线程联合,当某个线程等待另一个线程执行结算后,才能继续执行时,使用 join() 方法
-
死亡状态(Terminated)
线程正常执行完后死亡
执行 stop() / destory() 方法终止线程后线程死亡
线程方法:
- sleep():让线程进入阻塞状态,休眠时间到后再进入就绪状态
- yield():让线程直接进入就绪状态,让出CPU使用权
- join():联合,使线程1和线程2联合,在线程1中执行线程2的jion()方法,线程1必须等待线程2执行完毕才能继续执行
线程优先级(Priority)
优先级不代表绝对的执行顺序,而是概率,优先级高的可能会优先调用
优先级范围从1(MIN_RPIORITY) - 10(MAX_PRIORITY),默认为5(NORM_PRIORITY)
getPriority(),获取当前线程优先级
用户线程 / 守护线程
线程分为用户线程和守护线程
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程的执行完毕
守护线程一般后台记录操作日志/监控内存等,是为用户线程服务
线程同步
同步:协同步调,按预定的先后次序执行
并发:同一个对象被多个线程同时操作
synchronized:
- 同步方法
同步方法锁的是this,即对象本身或class - 同步块
synchronized(obj) {},obj称为同步监视器,可以任何对象,优先使用共享资源作为同步监视器
死锁
多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能进行,而导致两个或多个线程都在等待对方释放资源,都停止执行的。
死锁由同步块同时持有多个对象锁造成,避免死锁,即不要同时持有两个对象的锁
生产者/消费者模式
生产者:负责生成数据的模块(方法/对象/线程/进程)
消费者:负责处理数据的模块(方法/对象/线程/进程)
缓冲区:消费者不能直接使用生产者的数据,它们之间有个缓冲区,生产者将生产好的数据放入缓冲区中,消费者从缓冲区中拿数据
缓冲区是实现并发的核心,缓冲区的优势:
- 实现线程的并发协作
有缓冲区后,生产者只需要往缓冲区中放置数据,消费者只需从缓冲区中获取数据,实现生产者和消费者分离 - 解耦了生产者和消费者
- 解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费,消费者处理数据慢时,生产者仍可继续往缓冲区中放置数据
线程并发协作:
线程并发协作-线程通信,通常用于生产者/消费者模式:
1.生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
2.对于生产者,没有生产产品前,消费者需要进入等待状态,而生产了产品后,又需要马上通知消费者消费
3.对应消费者,在消费之后,需要通知生产者已经消费结算,需要继续生产以供消费
4.在生产者消费者问题中,仅仅synchronized是不够的
- synchronized可阻止并发更新同一个共享资源,实现同步
- synchronized不能用来实现不同线程之间的消息通信
5.线程消息通信方法:
- wait():线程持续等待
- wait(long time):线程等待指定时间
- notify():唤醒一个处于等待状态的线程
- notifyAll():唤醒同一个对象上所以调用wait()方法的线程,优先级别高的线程优先运行
public class ProductorOrConsumer {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
// 生产者
class Productor extends Thread {
SynContainer synContainer;
public Productor(SynContainer container) {
this.synContainer = container;
}
@Override
public void run() {
// 生产
for (int i = 0; i < 100; i++) {
System.out.println("生产--》 " + i + "号KFC");
synContainer.push(new KFC(i));
}
}
}
// 消费者
class Consumer extends Thread {
SynContainer synContainer;
public Consumer(SynContainer container) {
this.synContainer = container;
}
@Override
public void run() {
// 消费
for (int i = 0; i < 100; i++) {
KFC pop = synContainer.pop();
System.out.println("消费--》 " + pop.getFlapper() + "号KFC");
}
}
}
// 缓冲区
class SynContainer {
KFC[] kfcs = new KFC[10];
int count = 0;
// 生产
public synchronized void push(KFC kfc) {
// 数据满后等待
if(count == kfcs.length) {
try {
// 线程阻塞 需要消费者通知生产解除
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
kfcs[count] = kfc;
count++;
// 生产后通知消费解除
this.notifyAll();
}
// 消费
public synchronized KFC pop() {
// 没有数据后等待
if (count == 0) {
try {
// 线程阻塞 需要生产者通知消费解除
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
KFC k = kfcs[count];
// 消费后通知生产解除
this.notifyAll();
return k;
}
}
// KFC
class KFC {
private int flapper;
public KFC(int flapper) {
this.flapper = flapper;
}
public int getFlapper() {
return flapper;
}
public void setFlapper(int flapper) {
this.flapper = flapper;
}
}
Other
任务定时调度
通过Timer和Timetask,实现定时启动某线程
Timer本身就是线程,起调用其他线程的作用
public class TimerTest {
public static void main(String[] args) {
// 定义定时器
Timer t = new Timer();
// 定义任务
Task task = new Task();
// 3秒后执行
t.schedule(task, 3000);
// 3秒后执行,每个3秒执行一次
t.schedule(task, 3000, 3000);
}
}
class Task extends TimerTask {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
for (int j = 0; j <= i; j++) {
System.out.print("哈");
}
System.out.println("!!!");
}
}
}
quartz 任务进度管理器
scheduler - 调度器,控制所有调度
trigger - 触发器,采用DSL模式
JobDetail - 需要处理的JOB
Job - 执行逻辑
- DSL - Domain specific language领域特定语言,针对一个特定领域,具有受限表达性的一种计算机语言程序,即领域专用语言,声明式编程(简洁/连贯的代码使用,可以之间.方法,类似.append().append())
HappenBefore 指令重排规则
程序代码执行的顺序与编写的代码不一致,即虚拟机优化代码顺序,优化程序性能
- 在虚拟机层面,为了尽可能的减少内存操作速度远慢于cpu运行速度带来的cpu空置影响,虚拟机会按照自己的一些规则,将程序编写顺序重排,优先执行没有逻辑顺序的代码,提高性能
volatile
volatile是一个类型修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值
保证线程间变量的可见性,即当线程A对变量o修改后,线程A后面执行的其他线程能看到变量o的变动,即需要符合以下规则:
- 线程对变量进行修改后,要立刻回写到主内存
- 线程对变量读取的时候,要从主内存中读取,而不是缓存中
volatile保证数据的可见性,但不能保证原子性
可重入锁
锁可连续使用 - 锁作为并发共享数据保证一致性的工具,大多数内置锁都是可重入的,也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加1,而当线程退出同步锁代码块时,计数器将会递减,当计数值等于0时,锁释放。如果没有可重入锁的支持,在第二次企图获得锁时将会进入死锁状态。
CAS
悲观锁:synchronized-独占锁/悲观锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。
乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因冲突失败就重试,直到成功为止。 - 有当前内存值、原值、更新的值,若内存值和原值相同,即修改值并返回true,否则不做操作,返回false
CAS是一组原子操作,不会被外部打断,其属于硬件级别的操作,效率比加锁操作高,是利用cpu的cas指令,同时借助JNI来完成的非阻塞算法。