多线程简学
线程相关概念
进程:是计算机中程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。简单的理解就是操作系统中正在运行的一个程序。
线程:就是进程的一个执行单元,一个单一顺序的控制流。一个进程至少有一个线程(main函数),进程是线程的容器。每个线程都有自己的线程栈,自己的寄存器环境,自己的线程本地存储。
串行、并行、并发关系图
并行可以理解为严格更理想的并发
在java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)
Thread类有两个常用的构造方法:Thread()与Thread(Runnable)
定义Thread子类
public class ThreadDemo extends Thread {
定义一个Runnable接口的实现类
public class RunnableDemo implements Runnable{
这俩种创建线程的实质没有区别
主线程调用
public static void main(String[] args) {
ThreadDemo basic = new ThreadDemo();
basic.start();
new Thread(new RunnableDemo()).start();
}
start()方法来启动线程,但是start()的调用顺序并不一定是一定的,是线程调度器来分配执行顺序
线程的设置名称和获取名称 名称的作用主要是为了测试时使用
Thread.currentThread().getName() 获取线程名称
Thread.currentThread().setName("t1") 设置线程名称
isAlive方法测试线程活动状态
boolean alive = basic.isAlive();判断线程是否是激活状态
sleep()方法 让当前线程休眠毫秒数
Thread.sleep(1100);
getId()可以获得线程的唯一标识
long id = basic.getId();
yield()方法的作用是放弃当前CPU资源
basic.yield();
basic.setPriority(5); 设置线程的优先值 1到10
basic.setDaemon(true);守护线程 主线程结束后 子线程也会停止
线程的生命周期是线程对象的生老病死,即线程的状态,线程的生命周期可以通过getState()方法获得,线程的状态是枚举类型定义的 具体的可以查看javaApi Enum Thread.State
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
线程安全问题
非线程安全:主要是指多个线程对同一个对象的实例变量进行操作,会出现值被更改,值不同步的情况。
原子性:就是不可分割的意思,多个线程对共享变量进行操作时,要么已经开始 要么已经结束
访问(读写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发生,即其他线程到当前操作的中间结果,如从ATM机取款
class MyInt{
//java中提供了一个线程安全的AtomicInteger类 ,保证了操作的原子性
AtomicInteger integer = new AtomicInteger();
public int getInt(){
return integer.getAndIncrement();
}
可见性:一个线程对某个共享变量进行更新,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全问题的另外一种形式。
如果一个线程对共享变量更新后,后续访问该线程的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见。
java内存
锁概述
线程安全问题就是多个线程并发访问共享数据
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。
锁可以理解为对共享数据进行保护的一个许可证,任何线程想访问这些共享数据必须先持有该许可证。访问后释放其持有的锁(许可证)
锁具有排他性,即一个锁一次只能被一个线程使用。
JVM把锁分为内部锁和显示锁,内部锁通过synchronized关键字实现,显示锁通过java.concurrent.locks.Lock接口来实现
锁的作用
锁可以实现对共享数据的安全访问,保障线程的原子性、可见性和有序性
锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保障临界区的代码一次只能被一个线程执行。
锁的可重入性
如果一个线程持有一个锁的时候还能够继续申请该锁,称该锁是可重入的
void methodA(){
//申请A锁
methodB();
//释放A锁
}
void methodB(){
//申请A锁
.....
//释放A锁
}
内部锁:java中每个对象都有一个与之关联的内部锁(Intrinsic Lock) 这种锁也称为监视器。这种内部锁是一种排它锁,可以保障原子性。
内部锁是通过synchronized关键字实现的
修饰代码块:
synchronized(对象锁){
同步代码块,可以在同步代码块中访问共享数据
}
修饰方法
public void synchronized method(){
代码块
}
public class SynchronizedClass {
public static void main(String[] args) {
SynchronizedClass obj = new SynchronizedClass();
new Thread(()->obj.mm()).start();
new Thread(()->obj.mm()).start();
}
public void mm(){
synchronized (this){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}
锁对象不同不能实现同步
public class SynchronizedClass {
public static void main(String[] args) {
SynchronizedClass obj = new SynchronizedClass();
SynchronizedClass obj1 = new SynchronizedClass();
new Thread(()->obj.mm()).start();
new Thread(()->obj1.mm()).start();
}
public void mm(){
synchronized (this){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}
使用常量作为锁对象
public class SynchronizedClass {
public static void main(String[] args) {
SynchronizedClass obj = new SynchronizedClass();
SynchronizedClass obj1 = new SynchronizedClass();
new Thread(()->obj.mm()).start();
new Thread(()->obj1.mm()).start();
}
private static final Object obj = new Object();
public void mm(){
synchronized (obj){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
}
到底多线程 是怎么学的 坚持不下来 有点烦恼!!!!
今天下午先学习5章!
第一章
多线程的基础
首先理解一点:多线程是同时发生的么?
不是,只是cpu把工作时间分成很多很短的时间片,一个一个时间片的来执行任务,假如多个线程在这些时间片里面轮流执行,但是由于时间片都是很短的,用户根本察觉不到是轮流在执行,在用户看来就是同时在运行多个线程
--------来源自百度
CPU执行代码都是一条一条顺序执行的,但是,即使是单核CPU,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。
假如我们有语文、数学、英语3门作业要做,每个作业需要30分钟,我们把这3门作业看做3个任务,可以做1分钟语文作业,在做1分钟数学作业,在做一分钟英语作业,这样轮流做下去,在肉眼看来,做作业的速度就非常快,看上去就像同时在做3门作业。
/**
* @author 坏小子
* @date 2022/1/19 : 15:16
* @Description :TODO
*/
public class RunableText implements Runnable {
private String homeWork;
public RunableText() {
}
public RunableText(String homeWork) {
this.homeWork = homeWork;
}
public String getHomeWork() {
return homeWork;
}
public void setHomeWork(String homeWork) {
this.homeWork = homeWork;
}
结果
小明在这一段时间做语文
小明在这一段时间做英语
小明在这一段时间做数学
多线程的概念
一个任务就是一个进程,浏览器就是一个进程,视频播放器是另一个进程。
子任务就是线程
线程是操作系统调度的最小任务单位,有以下几种类型
-
多进程模式
-
多线程模式
-
多进程+多线程模式
总的来说就是:一个java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main方法,在main方法内部,我们又可以启动多个线程。
使用多线程有以下两种方式
-
继承Thread类
public static void main(String[] args) {
// for (int i = 0; i < 10; i++) {
// new Thread(new RunableText("语文")).start();
// new Thread(new RunableText("数学")).start();
// new Thread(new RunableText("英语")).start();
// }
Thread threadText = new ThreadText();
threadText.start();
}
static class ThreadText extends Thread{
-
创建
Thread
实例时,传入一个Runnable
此代码由 【Java 技术驿站】整理
// 多线程
----
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
-
线程的优先级
Thread.setPriority(int n) // 1~10, 默认值5
优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
线程的状态
线程的状态有以下几种:
New:新创建的线程,尚未执行;
Runnable:运行中的线程,正在执行run()方法的Java代码;
Blocked:运行中的线程,因为某些操作被阻塞而挂起;
Waiting:运行中的线程,因为某些操作在等待中;
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
Terminated:线程已终止,因为run()方法执行完毕
当线程启动后,它可以在Runnable
、Blocked
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。
线程终止的原因有:
线程正常终止:run()方法执行到return语句返回;
线程意外终止:run()方法因为未捕获的异常导致线程终止;
对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。
一个线程还可以等待另一个线程直到其运行结束。例如,main
线程在启动t
线程后,可以通过t.join()
等待t
线程结束后再继续运行:
public static void main(String[] args) {
System.out.println("start");
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new RunableText("美术"));
try {
thread.start();
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
new Thread(new RunableText("语文")).start();
new Thread(new RunableText("数学")).start();
new Thread(new RunableText("英语")).start();
System.out.println("end");
}
结果
start
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
小明在这一段时间做美术
end
小明在这一段时间做语文
小明在这一段时间做数学
小明在这一段时间做英语
当main
线程对线程对象t
调用join()
方法时,主线程将等待变量t
表示的线程运行结束,即join
就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main
线程先打印start
,t
线程再打印hello
,main
线程最后再打印end
。
如果t
线程已经结束,对实例t
调用join()
会立刻返回。此外,join(long)
的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()
方法,使得自身线程能立刻结束运行。
我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()
方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
public static void main(String[] args) throws InterruptedException{
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
static class MyThread extends Thread {
守护线程
此代码由 【Java 技术驿站】整理
Thread t = new MyThread();
t.setDaemon(true);
t.start();
如果有一个线程没有退出,JVM进程就不会退出,所以保证所有线程都能及时结束
如何创建守护线程呢?方法和普通线程一样,只是在调用start()
方法前,调用setDaemon(true)
把该线程标记为守护线程 注意 在调用start()方法之前调用setDaemon(true)
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.setDaemon(true);
t.start();
System.out.println("end");
}
static class MyThread extends Thread {
在来5章
线程同步
当多个线程同时运行时,线程的调度又操作系统决定,程序本身无法决定,因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后再某个时间后继续执行
这个时候,有个单线程模型不存在的问题就来了,如果多个线程同时读写共享变量,会出现数据不一致的问题
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
Thread r = new DecThread();
r.start();
t.start();
r.join();
t.join();
System.out.println(RunableText.count);
System.out.println("end");
}
static class MyThread extends Thread {
上面的代码很简单,两个线程同时对一个int
变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的
结果
510
end
这是因为对变量进行读写和写入时,必须保证时原子操作。原子操作是指不能被中断的一个或一系列操作
通过加锁和解锁的操作,仅能保证指令总工会是在一个线程执行期间,不会有其他线程会进入此指令区间,即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间,只有执行线程将锁释放后,其他线程才有机会获得锁并执行,这种加锁和解锁之间的代码快我们称之为临界区,任何时候临界区最多只有一个线程能执行
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
Thread r = new DecThread();
r.start();
t.start();
r.join();
t.join();
System.out.println(RunableText.count);
System.out.println("end");
}
static class MyThread extends Thread {
它表示用Counter.lock
实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。这样一来,对Counter.count
变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。
同步方法
我们知道java程序依靠synchronized
对线程进行同步,使用synchronized
的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized
逻辑封装起来。例如,我们编写一个计数器如下:
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count -= n;
}
}
public int get() {
return count;
}
}
这样一来,线程调用add()
、dec()
方法时,它不必关心同步逻辑,因为synchronized
代码块在add()
、dec()
方法内部。并且,我们注意到,synchronized
锁住的对象是this
,即当前实例,这又使得创建多个Counter
实例的时候,它们之间互不影响,可以并发执行
当我们锁住的是this
实例时,实际上可以用synchronized
修饰这个方法。下面两种写法是等价的:
-
1. ` public void add(int n) {`
2. ` synchronized(this) { // 锁住this`
3. ` count += n;`
4. ` } // 解锁`
5. ` }`-
1. ` public synchronized void add(int n) { // 锁住this`
2. ` count += n;`
3. ` } // 解锁`
-
因此,用synchronized
修饰的方法就是同步方法,它表示整个方法都必须用this
实例加锁。
我们再思考一下,如果对一个静态方法添加synchronized
修饰符,它锁住的是哪个对象?
对于static
方法,是没有this
实例的,因为static
方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class
实例,因此,对static
方法添加synchronized
,锁住的是该类的Class
实例
可重入锁
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
观察synchronized
修饰的add()
方法,一旦线程执行到add()
方法内部,说明它已经获取了当前实例的this
锁。如果传入的n < 0
,将在add()
方法内部调用dec()
方法。由于dec()
方法也需要获取this
锁,现在问题来了:
对同一个线程,能否在获取到锁以后继续获取同一个锁?
答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized
块,记录-1,减到0的时候,才会真正释放锁。
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态,当条件满足时,线程被唤醒,继续执行任务
/**
* @author 坏小子
* @date 2022/1/19 : 15:16
* @Description :TODO
*/
public class RunableText {
public static void main(String[] args) throws InterruptedException {
TaskQueue q = new TaskQueue();
ArrayList<Thread> ts = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread() {
public void run() {
// 执行task:
while (true) {
try {
String s = q.getTask();
System.out.println("execute task: " + s);
} catch (InterruptedException e) {
return;
}
}
}
};
t.start();
ts.add(t);
}
Thread add = new Thread(() -> {
for (int i = 0; i < 10; i++) {
// 放入task:
String s = "t-" + Math.random();
System.out.println("add task: " + s);
q.addTask(s);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
});
add.start();
add.join();
Thread.sleep(100);
for (Thread t : ts) {
t.interrupt();
}
}
}
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
有些仔细的童鞋会指出:即使线程在getTask()
内部等待,其他线程如果拿不到this
锁,照样无法执行addTask()
,肿么办?
这个问题的关键就在于wait()
方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object
类的一个native
方法,也就是由JVM的C代码实现的。其次,必须在synchronized
块中才能调用wait()
方法,因为wait()
方法调用时,会释放线程获得的锁,wait()
方法返回后,线程又会重新试图获得锁。
public synchronized String getTask() {
while (queue.isEmpty()) {
// 释放this锁:
this.wait();
// 重新获取this锁
}
return queue.remove();
}
当一个线程在this.wait()
等待时,它就会释放this
锁,从而使得其他线程能够在addTask()
方法获得this
锁。
现在我们面临第二个问题:如何让等待的线程被重新唤醒,然后从wait()
方法返回?答案是在相同的锁对象上调用notify()
方法。我们修改addTask()
如下:
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify(); // 唤醒在this锁等待的线程
}
注意到在往队列中添加了任务后,线程立刻对this
锁对象调用notify()
方法,这个方法会唤醒一个正在this
锁等待的线程(就是在getTask()
中位于this.wait()
的线程),从而使得等待线程从this.wait()
方法返回。
重点关注addTask()
方法,内部调用了this.notifyAll()
而不是this.notify()
,使用notifyAll()
将唤醒所有当前正在this
锁等待的线程,而notify()
只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)这是因为可能有多个线程正在getTask()
方法内部的wait()
中等待,使用notifyAll()
将一次性全部唤醒。通常来说,notifyAll()
更安全。
多线程是Java实现多任务的基础,Thread对象表示一个线程,我们可以在代码中调用Thread.currentThread()`获取当前线程。
对于多任务,java标准库提供的线程池可以方便的执行这些任务,同时复用线程,web应用程序就是典型的多任务应用,每个用户请求页面我们都会创建一个任务
类似:
public void process(User user) {
checkPermission();
doWork();
saveStatus();
sendResponse();
}
然后通过线性池去执行这些任务
观察process()
方法,它内部需要调用若干其他方法,同时,我们遇到一个问题:如何在一个线程内传递状态?
process()
方法需要传递的状态就是User
实例,有的童鞋会想,简单地传入User
就可以了
public void process(User user) {
checkPermission(user);
doWork(user);
saveStatus(user);
sendResponse(user);
}
但是往往一个方法又会调用其他很多方法,这样会导致User
传递到所有地方
这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。
给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User
对象就传不进去了。
Java标准库提供了一个特殊的ThreadLocal
,它可以在一个线程中传递同一个对象。
ThreadLocal
实例通常总是以静态字段初始化如下
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
使用方式如下
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}
通过设置一个User
实例关联到ThreadLocal
中,在移除之前,所有方法都可以随时获取到该User
实例
实际上,可以把ThreadLocal
看成一个全局Map
:每个线程获取ThreadLocal
变量时,总是使用Thread
自身作为key:
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
因此,ThreadLocal
相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal
关联的实例互不干扰
最后,特别注意ThreadLocal
一定要在finally
中清除:
try {
threadLocalUser.set(user);
...
} finally {
threadLocalUser.remove();
}
这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal
没有被清除,该线程执行其他代码时,会把上一次的状态带进去