1. 概念
- 程序:
- 指令和数据的有序集和,其本身没有任何运行的含义,是一个静态的概念
- 进程:
- 执行程序的一次执行过程,是一个动态的概念,是系统资源分配的单位
- 线程:
- 通常一个进程包含多个线程,当前一个进程至少有一个线程,不然没有存在的意义,线程是CPU电镀和执行的单位
2. 创建线程
方式一:继承Thead接口
// 1. 继承Thead类
public class TestThead extends Thread{
// 2. 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("多线程执行-----" + i);
}
}
public static void main(String[] args) {
// 3. 创建实例并执行start()方法
TestThead testThead = new TestThead();
testThead.start();
for (int i = 0; i < 500; i++) {
System.out.println("主线执行-----" + i);
}
}
}
方式二:实现Runnable接口(建议)
- 方便对象的复用
// 1,实现Runable接口
public class TestTherd implements Runnable{
// 2,重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("多线程执行 --- " + i);
}
}
public static void main(String[] args) {
// 3,创建对象实例并使用静态代理执行start方法
new Thread(new TestTherd()).start();
for (int i = 0; i < 200; i++) {
System.out.println("主线程执行 -- " + i);
}
}
}
方式三:实现Callable接口
/**
* @author: mingan.xie
* @since: 2020/6/23 20:12
* @email:
*/
// Callable实现多线程
// 1,实现Callable接口,泛型类型与call方法的返回值类型一致
public class TestCallable implements Callable<Boolean> {
// 2,重写call方法
@Override
public Boolean call() throws Exception {
System.out.println("线程执行了");
}
public static void main(String[] args) {
// 3,创建实例
TestCallable testCallable1 = new TestCallable();
TestCallable testCallable2 = new TestCallable();
TestCallable testCallable3 = new TestCallable();
// 4,创建执行服务
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 5,提交执行
Future<Boolean> future1 = executorService.submit(testCallable1);
Future<Boolean> future2 = executorService.submit(testCallable2);
Future<Boolean> future3 = executorService.submit(testCallable3);
// 6,关闭服务
executorService.shutdownNow();
}
}
3. lamda表达式
-
必须是函数式接口,即只有一个方法的接口
/** * @author: mingan.xie * @since: 2020/6/23 21:01 * @email: */ public class TestLamda { public static void main(String[] args) { EatRice eatRice = new EatRice() { @Override public void rice() { System.out.println("匿名内部类吃饭了"); } }; eatRice.rice(); eatRice = () -> { System.out.println("lamda表达式吃饭了"); }; eatRice = () -> System.out.println("lamda表达式吃饭了"); eatRice.rice(); } } interface EatRice{ void rice(); } class Me implements EatRice{ @Override public void rice() { System.out.println("吃饭了"); } }
4. 线程的6种状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
- 调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,此时处于就绪状(ready)。
- 就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
- 定时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
- 终止(TERMINATED):表示该线程已经执行完毕。
5. 线程的基本操作
停止线程(stop)
-
建议使用一个标志位来停止线程
/** * @author: mingan.xie * @since: 2020/6/25 8:24 * @email: */ // 停止线程: // 不建议手动停止线程,建议使用一个标志位来停止线程 public class TestStop implements Runnable{ private boolean flag = true; @Override public void run() { int i = 0; while (flag){ System.out.println("Thrad ---> " + i++); } } public void stop(){ flag = false; } public static void main(String[] args) { TestStop testStop = new TestStop(); new Thread(testStop).start(); for (int i = 0; i < 1000; i++) { if (i == 800){ testStop.flag = false; } System.out.println("main ---> " + i); } } }
线程休眠(sleep)
- sleep()指定当前线程阻塞的毫秒数
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每一个对象都有一把锁,sleep不会释放锁
线程礼让(yield)
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CUP重新调度,礼让不一定成功
/**
* @author: mingan.xie
* @since: 2020/6/25 8:52
* @email:
*/
public class TestYield implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程执行了 ---> 开始");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "线程执行了 ---> 结束");
}
public static void main(String[] args) {
TestYield testYield = new TestYield();
new Thread(new TestYield(),"a").start();
new Thread(new TestYield(),"b").start();
}
}
合并线程(join)
- 将几个并行线程的线程合并为一个单线程执行,让其他线程进入阻塞状态
- 等当前线程执行完毕之后,在执行其他线程
- 可以想象成插队
线程优先级(priority)
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度按照优先级决定应该调度那个线程执行
-
在调用star方法执行前设置优先级
/** * @author: mingan.xie * @since: 2020/6/25 9:20 * @email: */ public class TestPriority { public static void main(String[] args) { // 主线程优先级默认为5 System.out.println(Thread.currentThread().getName() + "main线程执行 ---》" + Thread.currentThread().getPriority()); MyPriority myPriority = new MyPriority(); Thread threada = new Thread(myPriority, "a"); threada.setPriority(1); threada.start(); Thread threadb = new Thread(myPriority, "b"); threadb.setPriority(Thread.NORM_PRIORITY); threadb.start(); Thread threadc = new Thread(myPriority, "c"); threada.setPriority(8); threadc.start(); Thread threadd = new Thread(myPriority, "d"); threadd.setPriority(Thread.MAX_PRIORITY); threadd.start(); } } class MyPriority implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程执行 ---》" + Thread.currentThread().getPriority()); } }
守护线程(daemon)
-
线程分为用户线程和守护线程
-
虚拟机必须再确认用户线程执行完毕后才能关闭,不用等待守护线程执行完毕
-
常见实例:日志操作,立即回收,内存监控
/** * @author: mingan.xie * @since: 2020/6/25 9:36 * @email: */ public class TestDaemon { public static void main(String[] args) { Thread thread = new Thread(new Word()); // 设置线程为守护线程,默认为false(用户线程) thread.setDaemon(true); thread.start(); // 主线程 new Thread(new You()).start(); } } class You implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("你开心的活着 ---> " + i + " 天"); } System.out.println("=============== good byd=============="); } } class Word implements Runnable{ @Override public void run() { while (true){ System.out.println("地球养育着你"); } } }
6. 线程同步
- 多个线程访问同一个对象,并且线程还需要修改对象时,就会引起并发问题,这个时候就就需要进行线程同步
- 线程同步就是一种等待机制,多个需要访问此对象的线程进入这个对象的等待池形成列队,等待前面的线程使用完毕,下一个线程再使用
- 由于同一个进程的多个线程共享同一块内存区域,为了保证数据的准确性,在访问时加入锁机制
- 当一个线程获取到对象的排它锁,它就会独占资源,其他线程必须等待
- 缺点:
- 一个线程持有锁,回到吃其他需要此对象的线程挂起
- 在多线程的竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
同步方法 synchronized
- 控制每个对象的访问,每个对象对应一把锁,每个sunchronized方法必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,放到方法结束才会释放锁
同步块
- synchronized(obj){}
- obj:同步监视器
- obj可以使任意对象,但是推荐使用共享资源作为同步监视器
- 同步方法无需指定同步监视器,因为同步方法的同步监视器就是this,就是对象本身,或者是class
- 同步监视器的执行过程:
- 第一个线程被访问,锁定同步监视器,执行其中代码
- 第二个线程被访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
死锁
- 多个线程各自占有一些资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对象释放资源,都停止运行
- 某一个同步块同时拥有两个以上对象的锁时,就有可能发生死锁的问题
- 产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获取的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
Lock(锁)
-
大佬博客:https://www.cnblogs.com/linghu-java/p/8944784.html
-
偏向锁:
- 它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
- 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁
-
轻量级锁(CAS):
- 对数据就行修改前先进行一次查询,在修改时对数据进行比较,如果相同则修改,反之则重复这个过程
- ABA问题:即数据是相同的,但还是可能已经被其他线程修改过了
-
解决方案:使用版本号或者时间戳
-
ABA的版本号和时间戳如何保证原子性:
AtomicInteger atomicInteger = new AtomicInteger(1); atomicInteger.incrementAndGet();
-
-
自旋锁:
- 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
- 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗
- 但是要注意设置自旋锁的最大时间,合理的时间时一次CPU切换上下文的时间
-
重量级锁(synchronized):
-
有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- !Owner:当前释放锁的线程。
-
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
-
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
-
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
-
// 通过显示定义同步锁对象来实现同步,同步锁使用lock兑现充当
// ReentrantLock 类实现了lock,他拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的Reentrantlock,可以显式加锁,释放锁
/**
* @author: mingan.xie
* @since: 2020/6/27 10:28
* @email:
*/
public class ReenTantLock {
public static void main(String[] args) {
Tickets tickets = new Tickets();
new Thread(tickets,"A").start();
new Thread(tickets,"B").start();
new Thread(tickets,"C").start();
}
}
class Tickets implements Runnable{
private int num = 10;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();
Thread.sleep(200);
if (num > 0){
System.out.println(num--);
}else{
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
Volatile 关键字
-
大佬博客:https://blog.csdn.net/it_dx/article/details/70045286
-
基本概念:
- 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:即程序执行的顺序按照代码的先后顺序执行。
-
Volatole 作用:保证内存可见性,禁止指令重排序
- 可见性:
- 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
- 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
- 有序性:
- Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
- 可见性:
7. 线程通信
- 这是一个线程同步的问题,生产这和消费者共享一个资源,并且生产者和消费者之间相互依赖,互为条件
- 对于生产者没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知消费者已经结束消费,需要生产新的产品
- wait():表示线程处于等待,会释放锁,需要其他线程唤醒
- notifyAll():唤醒同一个对象上的所有调用wati()方法的线程
管程法
-
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
package com.demo06; /** * @author: mingan.xie * @since: 2020/6/27 10:56 * @email: */ public class TestPC { public static void main(String[] args) { Buffer buffer = new Buffer(); new Thread(new Producer(buffer)).start(); new Thread(new Consumer(buffer)).start(); } } // 生产者 class Producer implements Runnable{ Buffer buffer; public Producer(Buffer buffer) { this.buffer = buffer; } @Override public void run() { for (int i = 0; i < 100; i++) { buffer.push(new Chicken(i)); System.out.println("生产了第:" + i+"只"); } } } // 消费者 class Consumer implements Runnable{ Buffer buffer; public Consumer(Buffer buffer) { this.buffer = buffer; } @Override public void run() { for (int i = 0; i < 100; i++) { Chicken pop = buffer.pop(); System.out.println("消费了第 ===》 "+pop.id+"值"); } } } // 产品 class Chicken{ int id; public Chicken(int id) { this.id = id; } } // 缓冲区 class Buffer implements Runnable{ Chicken[] buffers = new Chicken[10]; int num = 0; synchronized void push(Chicken chicken){ // 如果容器满了,就进入等待状态 if (num == buffers.length){ try { this.wait(); return; } catch (InterruptedException e) { e.printStackTrace(); } } // 如果没有满,就添加 buffers[num] = chicken; num++; // 通知消费者消费 this.notifyAll(); } synchronized Chicken pop(){ // 判断是否容器是否空了,就进入等待状态,等生产者生产 if (num == 0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 消费 num--; Chicken buffer = buffers[num]; // 唤起生产者生产产品 this.notifyAll(); return buffer; } @Override public void run() { } }
信号灯法
- 通过一个标识符来判断当前生产消费的状态
package com.demo06;
/**
* @author: mingan.xie
* @since: 2020/6/27 11:32
* @email:
*/
// 线程通信方法:信号灯法
public class TestPC2 {
public static void main(String[] args) {
Program program = new Program();
new Thread(new Perform(program)).start();
new Thread(new Audience(program)).start();
}
}
// 生产者
class Perform implements Runnable {
Program program;
public Perform(Program program) {
this.program = program;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
program.play("新闻联播");
} else {
program.play("花样广告");
}
}
}
}
// 消费者
class Audience implements Runnable {
Program program;
public Audience(Program program) {
this.program = program;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
program.wacth();
}
}
}
class Program implements Runnable {
// 控制状态的切换
boolean flag = true;
String view;
synchronized void play(String view) {
// flag: t -> 生产 、 f -> 消费
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产者生产
System.out.println("演员生产了" + view);
this.view = view;
this.flag = !this.flag;
// 唤醒消费者消费
this.notifyAll();
}
synchronized void wacth() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费者消费
System.out.println("消费者观看了:" + view);
this.flag = !this.flag;
// 唤醒生产者生产
this.notifyAll();
}
@Override
public void run() {
}
}
8. 线程池
- 背景:
- 经常创建和销毁,使用量特别大的资源,比如说并发情况下的线程,对性能影响很大
- 可以避免频繁的创建和销毁,实现重复利用
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
package com.demo06;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: mingan.xie
* @since: 2020/6/27 12:14
* @email:
*/
public class TestPool {
public static void main(String[] args) {
MyThread myThread = new MyThread();
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(myThread);
service.execute(myThread);
service.execute(myThread);
service.execute(myThread);
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}