定义
进程,线程,和程序
- 程序
Program
- 未完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象
- 进程
Process
- 程序的一次执行过程,或是正在运行的程序,是一个动态的过程,有自身产生,存在,和消亡的过程
- 生命周期
- 运行中的 QQ,运行中的 MP3
- 进程不能控制自身的资源分配,系统运行时给其动态分配资源
- 程序的一次执行过程,或是正在运行的程序,是一个动态的过程,有自身产生,存在,和消亡的过程
- 线程
Thread
- 程序内部的一条执行路径
- 若一个进程同一时间内并行多个线程,那么其支持多线程
- 线程是调度和执行的单位,每个线程有独立的栈和程序计数器,线程的切换开销小
- 多个线程共享一个进程中的方法区和堆
- 这也就意味这,多个线程可以同时操作堆和方法区中的数据,造成的潜在的安全隐患
单核和多核CPU
- 单核
CPU
- 假的多线程,因为在一个时间单元内,只能执行一个线程的任务
- 因此,
CPU
总是在挂起某些程序;因为CPU
的时间单元(切换的速度)特别短,所以感觉不出来 - 多核
CPU
才能更好发挥多线程的效率 main()
主线程,gc()
垃圾回收线程,异常处理线程 – 一个java程序至少有3个线程
- 并行
- 多个
CPU
同时执行多个任务,比如多个人做不同的事情;就好像是多车道
- 多个
- 并发
- 一个
CPU
利用时间片同时执行多个任务;就好像一个加油站,每次收费时总是挂起其他在公路上的客户;只不过挂起的切换速度快,你感受不到
- 一个
Why
-
优点
- 对于单核
CPU
而言,如果只用单线程,运行的效率反而更快,因为这样CPU
就不用切换;反之,多线程时,CPU
就需要不停的切换,反而更慢 - 提高计算机程序的相应,尤其是图像化页面 – 用户输入一个线程,后台计算一个线程;这可以提升用户的体验,,因为一个用户可能在一个时间内做多个事情,而不像
dos
,一个时间只做一个 - 提高计算机系统
cpu
的利用率 - 改善程序结构,将长而复杂的进程分成了多个线程,独立运行,利于理解和修改
- 对于单核
-
合适
- 程序需要同时执行两个或多个任务
- 程序实现一些需要等待的任务,如用户输入,网络搜索,文件读写操作
- 比如,让文件读写操作当线程运行,使用同步IO将会浪费资源并可能造成卡顿,如美团外卖的滑动
-
需要一些后台运行的程序时
线程的分类
Java
中的线程分为两类:一种是守护线程,一种是用户线程。- 它们在几乎每个方面都是相同的,唯一的区别是判断
JVM
何时离开 - 守护线程是用来服务用户线程的,通过在
start()
方法前调用thread.setDaemon(true)
可以把一个用户线程变成一个守护线程。
Java
垃圾回收就是一个典型的守护线程。- 若
JVM
中都是守护线程,当前JVM
将退出。 - 形象理解: 兔死狗烹,鸟尽弓藏
- 它们在几乎每个方面都是相同的,唯一的区别是判断
线程的创建和启动
- 如果一个程序的执行过程可以用一条线画出来,那么就是单线程
Extends java.lang.Thread
-
Code
-
public class ThreadTestExtend2 { public static void main(String[] args) { MyStringTest t = new MyStringTest(); t.start(); } } class MyStringTest extends Thread { @Override public void run() { ... } }
- 创建
Thread
子类 - 重写
run()
,这个函数中包含这个线程做什么 - 实例化该子类
- 调用
Thread
类中start
- 创建
-
-
问题
-
不能直接调用
ThreadSubClassInstance.run()
来启动一个线程- 上述方法等同于
main
中调用普通方法
- 上述方法等同于
-
对于同一个线程对象,不能调用两次
start()
-
if (threadStatus != 0) throw new IllegalThreadStateException(); //如上所示,一个线程对最多被 start 一次;否则得到 IllegalThreadStateException
-
但是你可以重新创建一个线程的对象
-
-
Anonymous Class
- 创建
Thread
匿名子类可以实现快速的写多线程
- 创建
-
implements Runnable
:happy:
-
Code
-
public class ThreadTestImplement { public static void main(String[] args) { new Thread(new MThread()).start(); } } class MThread implements Runnable{public void run() {...}}
-
创建一个实现了
Runnable
接口的类 -
实现类实现
run()
抽象方法 -
实例化类的对象
-
将
Runnable
对象作为参数传入Thread
类的构造器,创建Thread
类对象并运行start()
-
public Thread(Runnable target){this(null, target, "Thread-" + nextThreadNum(), 0);}
-
-
上述方法可以成功,因为
private Runnbale target
将在使用上述方法时赋值,run()
其实就是调用了target
中的run
方法;这就是不重写同样成功的原因
-
-
-
这个方法是被建议的
implement
是可以做
的关系,而继承是是一个
;这样的操作更符合直觉- 实现的方式天然实现了共享数据,不需要使用
static
关键字 - 除此以外,这个方式还可以实现某种意义上的多继承
- 他们其实都是为了修改
run
方法
Callable
-
Since JDK 5.0
-
与使用
Runnable
相比,Callable
的功能更加强大- 相比
run()
方法,可以有返回值 - 方法可以抛出异常
- 一旦抛出异常,结果就是当前线程终止运行;但是你不能直接捕获那个线程抛出的异常,最多得到
ExecutionException
,里面的getMessage
可以让你知道抛出的异常是什么
- 一旦抛出异常,结果就是当前线程终止运行;但是你不能直接捕获那个线程抛出的异常,最多得到
- 支持泛型的返回值
- 借助
FutureTask
类,可以获取返回结果
- 相比
-
Code
-
public class CallableSoldTicket { public static void main(String[] args) { Window w = new Window();//实例化实现类 FutureTask<Integer> f1 = new FutureTask<Integer>(w); //创建 FutureTask对象 Thread w1 = new Thread(f1);//创建新线程 w1.start(); int o1 = 0; try {o1 = f1.get();}//得到返回值 catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();} //如果出现异常,catch 到 ExecutionException;内部包含信息有实现类中抛出的异常 System.out.println(o1); //输出返回值 } } class Window implements Callable<Integer> {//创建 Callable 接口的实现类 private int ticket = 100; private volatile boolean flag = true; private ReentrantLock lock = new ReentrantLock(); @Override public Integer call() throws Exception { long start = System.currentTimeMillis(); while (flag) { try {lock.lock(); if (ticket > 0) { System.out.println(String.format("窗口: %s,卖出票: %d", Thread.currentThread().getName(), ticket--)); try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();} } else {flag = false;} } finally { lock.unlock();} } long end = System.currentTimeMillis(); return Integer.valueOf((int) (end - start)); } }
- 创建
Callable
接口的实现类 - 实现
call
方法,将此线程需要执行的操作声明在此方法中,允许返回值和抛出异常 - 创建
Callable
接口实现类对象,并将其作为参数传入到FutureTask
类的构造器,并启动将FutureTask
类作为参数传入Thread()
的构造器,并运行 - 最后
futureTask.get
获取返回值
- 创建
-
ThreadPool
-
背景
- 创建和销毁线程本身意味着巨大的性能开销
-
思路
- 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
-
好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止
-
例子
JDK 5.0
起提供了线程池相关API:ExecutorService
和Executors
ExecutorService
- 真正的线程池接口。常见子类
ThreadPoolExecutor
void execute(Runnable command)
- 执行任务/命令,没有返回值,一般用来执行
Runnable
- 执行任务/命令,没有返回值,一般用来执行
<T> Future<T> submit(Callable<T> task)
执行任务,有返回值,一般又来执行Callable
void shutdown()
关闭连接池
- 真正的线程池接口。常见子类
Executors
工具类、线程池的工厂类,用于创建并返回不同类型的线程池Executors.newCachedThreadPool()
创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(n)
创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n)
创建一个线程池,它可安排在给定延迟后运
行命令或者定期地执行
-
Code
-
public class ThreadPoolTest2 { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(4); FutureTask f = new FutureTask<Integer>(new myThread()); service.submit(f); service.submit(new FutureTask<Integer>(new myThread())); service.submit(new FutureTask<Integer>(new myThread())); try { System.out.println(f.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } service.shutdown(); } } class myThread implements Callable<Integer> { @Override public Integer call() throws Exception { int sleepTime = new Random().nextInt(100); String whoAmI = Thread.currentThread().getName(); System.out.println(whoAmI + ":" + sleepTime); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } return sleepTime; } }
- 使用
Executors
工厂类中提供的静态方法创建线程池 - 使用返回的线程池对象,
submit()
来添加一个Callable
线程,execute()
来添加一个Runnable
或者Callable
线程 - 关闭线程池
- 使用
-
线程常用方法
-
void start()
- 启动线程,并执行对象的
run()
方法
- 启动线程,并执行对象的
-
run()
-
调用线程时执行的操作
-
需要重写此方法,否则调用默认方法
-
public void run() { if (target != null) { target.run(); } }
-
-
<ThreadInstance>.getName()
-
当前线程的名称
-
自动起名的原因
-
public Thread() { this(null, null, "Thread-" + nextThreadNum(), 0); } //The above explains the Thread-0,1,2... names public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { this(group, target, name, stackSize, null, true); } //stackSize is a suggestion to JVM virtual machine. It may improve stack depth and prevent StackOverflowError if set high; or allowing more thread run concurrently and prevent OutOfMemoryError. However, no guarantee are here for this behavior, and highly dependent on Platform & JRE Implementation
-
-
Thread.currentThread()
- 静态方法,获得当前线程
-
<ThreadInstance>.setName(String name)
-
实例方法,修改当前线程的名字
-
修改名字应当在调用
start()
方法之前,否则可能出现线程名在运行过程中/结束后突然改变 -
public Thread(String name) { this(null, null, name, 0); } //这个方法允许你在创建自己线程的时候直接指定名字
-
-
Thread.yield()
- 静态方法
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
-
Thread.join()
-
线程A中使用
B.join()
,则 A 开始执行直到 B 执行完毕 -
会抛出
InterruptedException
-
public final synchronized void join(final long millis)//重写方法 join() == join(0) throws InterruptedException { if (millis > 0) { //如果等待时间大于0,则只要加入的线程未死,且执行时间不大于 delay,一直等待;while的方法保证等够时间 if (isAlive()) { final long startTime = System.nanoTime(); long delay = millis; do { wait(delay); } while (isAlive() && (delay = millis - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0); } } else if (millis == 0) { while (isAlive()) {//如果等待时间等于零,则无限等待直到线程执行完毕 wait(0); } } else { throw new IllegalArgumentException("timeout value is negative"); } }
-
例子
-
Thread even = new Thread() { @Override public void run() { for (int i = 0; i <= 10; i += 2) { System.out.println(i + "\t:\t" + Thread.currentThread().getName()); } } }; even.start(); for (int i = 1; i < 5; i++) { System.out.println(i + "\t:\t" + Thread.currentThread().getName()); if (i == 2) { try { even.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } //Output //1 : main //2 : main //这里, even.join()方法被调用 //0 : Thread-0 //2 : Thread-0 //4 : Thread-0 //6 : Thread-0 //8 : Thread-0 //10: Thread-0 //3 : main //4 : main
-
-
static void sleep(long millis)
- 让活动线程在指定时间段内放弃对
CPU
资源的控制,其他线程不论优先级大小随即执行 throw InterruptedException
, 如果其他的线程惊扰了这个线程
- 让活动线程在指定时间段内放弃对
-
isAlive()
判断线程是否活动 -
@deprecated, stop()
强制结束线程- 因为
stop()
方法在调用时,会抛出ThreadDeathError
;这个错误不会产生任何提示,并且将会杀死线程并释放其持有的所有锁;因此,这可能造成多线程安全问题 - 建议使用
volatile
关键字作为flag
,让对象周期性检查flag
以判断自己是否应该继续运行 - 或者使用
interrupt()
方法
- 因为
-
java.lang.object
wait()
- 和
sleep()
的异同- 一旦执行方法,都可以使当前的线程进入阻塞状态
- 两个方法声明的位置不同,
Thread
类中声明了sleep()
,而Object
类中声明wait()
- 两个方法调用的要求不同,
sleep()
可以在任何场景下调用,但是wait()
必须在同步代码块中调用 sleep()
调用后不释放锁,但是wait()
调用之后将会释放锁
- 和
notify()
notifyAll()
线程的调度
-
时间片
-
每个进程轮流运行
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0qhpSQWu-1633987865100)(笔记.assets/image-20211010223606744.png)]
-
-
抢占式
- 高优先级的线程抢占
CPU
- 高优先级的线程抢占
-
Java
的调度方法- 同优先级的线程
FIFO
先到先服务,使用时间片策略 - 高优先级的线程使用优先调度的抢占式策略
- 同优先级的线程
-
常量
MAX_PRIORITY = 10
NORM_PRIORITY = 5
MIN_PRIORITY = 1
-
方法
public int getPriority()
返回优先级public setPriority(int newPriority)
- 这个优先级必须在
[1,10]
之间,否则IllegalArgumentException
- 这个优先级必须在线程组的最大优先级一下,否则将被自动改为线程组的最大优先级
- 这个优先级必须在
- 优先级的高低并不一定决定某个线程必须向执行
- 我们只能说优先级高的线程,更有可能被执行;这只是个概率,而不是保证
生命周期
- 某一个对象,从出生到消亡的过程,称为线程的声明周期
- 线程的状态,这部分内容被定义在
Thread.State
的枚举类中NEW
一个创建,且尚未开始的线程RUNNABLE
可以被执行,已经调用了start()
;等待虚拟机对其分配资源BLOCKED
等待线程锁,以进入一个同步方法块WAITING
等待其他线程执行Object.notify() / Object.notifyAll()
,自己调用了wait() / join
以进入此状态TIMED_WAITING
在一定时间段内等待,因为使用了sleep()/wait(long)/join(long)
TERMINATED
一个线程已经执行完run()
方法,则死亡
- 不妨简化为5种情况
线程的通信
wait()
令当前线程挂起并放弃CPU
、 同步资源并等待, 使别的线程可访问并修改共享资源;等候其他线程调用notify()
或notifyAll()
方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行notify()
唤醒正在排队等待同步资源的线程中优先级最高者,即被wait()
的线程notifyAll()
唤醒所有正在排队等待资源的所有线程- 注意事项
- 这三个方法只有在
synchronized
方法或synchronized
代码块中才能使用,否则会报异常 - 这三个方法的调用者必须是同步监视器,否则出现问题
java.lang.IllegalMonitorStateException
- 而任意对象都可以作为
synchronized
的同步锁 - 这三个方法只能在
Object
类中声明
- 这三个方法只有在
同步实现
why
- 多个线程同时读写共享数据,有可能会造成线程的安全问题;因此,如果单线程或者没有共享数据,不会有线程的安全问题
- 这是因为多个线程读取线程内部缓存导致各个线程之间的修改不完全可见透明;每个线程也不一定能够在每一个
CPU
分配的时间片中将需要同步的代码执行完(也就是,执行一半未完成修改时被挂起);每个线程执行的顺序不一定能够如代码中所写顺序一样所致 - 如
- 银行取钱,两人同时取一个账户(共享数据)中的钱
- A判断通过,挂起;B判断通过,挂起;最后造成 A, B 从账本中同时取出两份钱,而两份钱可能大于剩余余额的问题
- 尽管上述情况出现的概率很小,我们仍然需要解决这个问题
解决
- 当一个线程操作共享数据时,其他数据不能参与进来,直到该线程操作完,其他的线程才可以继续操作
ticket
– 阻断其他线程
synchronized
同步代码块
-
synchronized(Object Monitor){...}
-
操作共享数据的代码,就是需要被同步的代码
-
共享数据,即多个线程共同操作的变量,如
实例变量
和类的静态变量 -
同步监视器
<=>
锁- 任何一个类的对象都可以充当锁,甚至是一个
new Object()
都可以 - 因为有且仅有一个对象,因此,所有的线程只能共用同一把锁;非如此不能将多线程中的关键部分转化为单线程,线程安全问题不会解决
- 如果所有的线程都有一把锁,那么问题没有解决
- 任何一个类的对象都可以充当锁,甚至是一个
同步方法
-
同步监视器
-
非静态方法,同步监视器等同于
-
synchronized(this){...}
-
-
因此,在处理
extends
方式实现的多线程时,必须将方法改成静态方法 – 存在多个对象;否则将会出现每个线程各自持有一个锁,导致线程安全问题- 静态方法,同步监视器将会是
Window4.class
(当前类本身) - 因为类是唯一的,所以修好了
- 静态方法,同步监视器将会是
-
需要考虑多个线程是否共用同一把锁!
-
同步懒汉式
-
class Bank { private Bank() {} private volatile static Bank instance = null; public static Bank getInstance() { if (instance == null) { synchronized (Bank.class) { if (instance == null) { instance = new Bank();} } } return instance;// 没有对共享数据进行结构性操作 } }
-
如果第一个线程进入并判断以后,创建了对象之后,不妨直接将
instance
返回给其他所有线程,而不用让他们也进入同步方法进行判断 -
volatile
可以提升效率,使得在操作数据的一瞬间就快速把instance
给写入主存- 但是单独使用
volatile
是不够的,因为instance = new Bank()
不是原子操作,创建bank
可能会出现多个
- 但是单独使用
死锁
-
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 例如,两个人吃饭,但是只有一双筷子;两个人没人拿了一个筷子,并且都等待对方放弃筷子,于是就出现了谁也别吃的问题。
-
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
-
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
-
需要注意的是,死锁可能出现而不被发现 – 这需要仔细的编写代码!
-
例子
-
package com.atguigu.java; class A { public synchronized void foo(B b) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法"); // ① try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last方法"); // ③ b.last(); } public synchronized void last() { System.out.println("进入了A类的last方法内部"); } } class B { public synchronized void bar(A a) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar方法"); // ② try { Thread.sleep(200); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法"); // ④ a.last(); } public synchronized void last() { System.out.println("进入了B类的last方法内部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主线程"); // 调用a对象的foo方法 a.foo(b); System.out.println("进入了主线程之后"); } public void run() { Thread.currentThread().setName("副线程"); // 调用b对象的bar方法 b.bar(a); System.out.println("进入了副线程之后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); new Thread(dl).start(); dl.init(); } }
- 我们发现,线程
main
想要运行下去必须要同时获得两个同步监视器a,b
- 线程 副线程想要运行下去必须或等两个同步监视器
b,a
- 于是死锁就发生了
- 我们发现,线程
-
Lock
-
Since JDK 5.0
-
java
显示的定义了一个同步锁对象来实现同步 -
java.util.concurrent.locks.Lock
接口是控制多个线程堆共享资源进行访问的工具;一个典型的实现有ReentrantLock
,可以显示加锁,释放锁
-
步骤
-
创建唯一的
ReentrantLock
对象 -
try{ lock.lock(); ...//The code that need to synchronized, here }finally{ lock.unlock(); }
- 使用上述格式保证解锁,即使存在
return
或其他可能的异常 lock
是必须要手动释放监视器的,而synchronized
可以自动在执行完后释放同步监视器
- 使用上述格式保证解锁,即使存在
-
-
lock
建议使用,引起更加灵活
利弊
- 上述方法解决了线程的安全问题
- 加上同步以后,只能有一个线程参与,其他的线程等待;变成了一个效率低下的单线程 – 这是一个局限性
- 因此,只框选需要同步的代码,不要多选或者少选
- 多选造成性能问题
- 少选造成同步失败
Volatile
-
volatile
Volatile
可以看做是轻量级的Synchronized
,它只保证了共享变量的可见性(一定的有序性。- 在线程 A 修改被
volatile
修饰的共享变量之后,线程 B 能够读取到正确的值。volatile
阻止- 线程内部缓存和重排序,即直接修改内存
- 从内存中直接读取,从内存中直接修改
synchronized
和lock
也可以保证可见性,因为他们保证- 同一时刻只有一个线程获得锁并执行代码
- 释放锁之前,变量的修改会被刷新到内存中
-
三大概念
-
可见性
- 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是线程修改的结果另一个线程马上就能看到。
-
原子性
-
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
-
Java
内存模型只保证了基本读取和赋值是原子性操作 -
public class VolatileTest { public static void main(String[] args) { final Test test = new Test(); for (int i = 0; i < 10; i++) { new Thread() { public void run() { for (int j = 0; j < 1000; j++) test.increase();//因为 volatile不保证原子性, 因此不保证 inc++会在同一个线程中被计算,并写入memory;因此,设想下列情况 A 读取 inc, 得到 10,中断;B 读取 inc,得到 10,因为A尚未写入memory,计算并写入 inc = 11; A得到资源, 得到 inc = 11;因此,inc的结果总是 < 10000 }; }.start(); } while(Thread.activeCount()>1) { //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } } } class Test { public volatile int inc = 0; public void increase() { inc++; } }
-
-
有序性
- 处理器可能会修改代码的执行顺序,以提高效率
- 保证运行结果一致,但是不保证执行顺序一致
volatile
相当于内存栅栏,保证- 运算
volatile
时,,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行; - 不能将在对
volatile
变量的读操作或者写操作的语句放在其后面执行,也不能把volatile
变量后面的语句放到其前面执行。 - 但是不保证前面的操作顺序正确,后面的操作顺序正确
- 运算
-
-
使用场景
- 对变量的写操作不依赖于当前值 – 非自增,自减
- 该变量没有包含在具有其他变量的不变式中 – 赋值不依赖其他变量
- 表示一个
concurrentflag
等等时很好的,因为速度更快
AtomicInteger
, AtomicLong
等
- 这些保证了自增自减预算的原子性,是一些特别的类,值得使用