7.1、线程基本知识
进程与线程:
进程(Process)是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。
在 windows 下查看进程的命令:
- 查看进程:tasklist
- 杀死进程:taskkill /pid <PID>
在 linux 下查看进程的命令:
- 查看进程:ps -fe
- 查看线程:top -H -p <PID>
- 杀死进程:kill <PID>
在 java 中查看进程的命令:
- 查看Java进程:jps
- 查看Java线程:jstack <PID>
- 图形化的工具:jconsole
并行与并发:
单核 CPU 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 CPU 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 CPU 在线程间的切换非常快,人类感觉是同时运行的 。总结为一句话就是:微观串行,宏观并行,一般会将这种线程轮流使用 CPU 的做法称为并发(Concurrent)。
多核 CPU下,每个核都可以调度运行线程,这时候线程可以是并行(Parallel)的。
同步与异步:
以调用方角度来讲:
- 需要等待结果返回后才能继续运行就是同步。
- 不需要等待结果返回就能继续运行就是异步。
7.2、线程创建方式
- 方式一:
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("thread run ...");
}
};
thread.start();
简化后:
Thread thread = new Thread(() -> System.out.println("thread run ..."));
thread.start();
- 方式二:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run ...");
}
});
thread.start();
简化后:
Thread thread = new Thread(() -> System.out.println("runnable run ..."));
thread.start();
- 方式三:
Callable<Integer> callable = new Callable() {
@Override
public Object call() throws Exception {
System.out.println("callable run ...");
return 521;
}
};
FutureTask futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
简化后:
Thread thread = new Thread(new FutureTask(() -> {
System.out.println("callable run ...");
return 521;
}));
thread.start();
7.3、线程基本方法
注意:标黄色的方法代表是
static
方法,可直接类名调用,无需创建对象。
7.4、线程安全问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
public class Main {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
以上的结果可能是正数、负数、零。
一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问共享资源:多个线程读共享资源其实也没有问题,问题本质在于多个线程对共享资源读写操作时发生指令交错,就会出现问题。
一段代码块内,如果存在对共享资源的多线程读写操作,称这段代码块为临界区(Critical Section)。
例如,下面代码中的临界区:
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件(Race Condition)。
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
我们使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,以此避免线程安全问题。
虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized的语法:
synchronized(对象)
{
临界区
}
synchronized改进后:
public class Main {
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
方法上的 synchronized
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
1、 如果只有读操作,则线程安全
2、 如果有读写操作,则这段代码是临界区,需要考虑线程安全
方法上(内)局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
1、 如果该对象没有逃离方法的作用访问,它是线程安全的
2、 如果该对象逃离方法的作用范围,需要考虑线程安全
7.5、线程并发工具
7.5.1、ReentrantLock
锁介绍
ReentrantLock 相对于 synchronized,它具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
基本语法:
// 创建锁
ReentrantLock reentrantLock = new ReentrantLock();
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入
可重入是指同一个线程,如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
public class Main {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
System.out.println("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
System.out.println("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
System.out.println("execute method3");
} finally {
lock.unlock();
}
}
}
execute method1
execute method2
execute method3
可打断
ReentrantLock 支持可打断,如果是不可中断模式,即使使用了 interrupt 也不会让等待中断。
public class Main {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("t1等锁的过程中被打断");
return;
}
try {
System.out.println("t1获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("main获得了锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
System.out.println("main执行打断");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
main获得了锁
t1启动...
main执行打断
t1等锁的过程中被打断
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.company.Main.lambda$main$0(Main.java:12)
锁超时
public class Main {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1启动...");
if (!lock.tryLock()) {
System.out.println("t1获取失败,返回");
return;
}
try {
System.out.println("t1获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("main获得了锁");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
main获得了锁
t1启动...
t1获取失败,返回
公平锁
ReentrantLock 默认是不公平的,改为公平锁语法如下,公平锁一般没有必要,会降低并发度。
ReentrantLock lock = new ReentrantLock(true);
多条件
synchronized 中也有条件变量,就是 waitSet 休息室,当条件不满足时进入 waitSet 等待,ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
public class Main {
public static ReentrantLock lock = new ReentrantLock();
public static Condition waitCigaretteQueue = lock.newCondition();
public static Condition waitbreakfastQueue = lock.newCondition();
public static volatile boolean hasCigrette = false;
public static volatile boolean hasBreakfast = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
while (!hasCigrette) {
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("小明等到了它的烟");
} finally {
lock.unlock();
}
},"小明").start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("小红等到了它的早餐");
} finally {
lock.unlock();
}
},"小红").start();
Thread.sleep(1000);
sendBreakfast();
Thread.sleep(1000);
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
System.out.println("送烟来了");
hasCigrette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
System.out.println("送早餐来了");
hasBreakfast = true;
waitbreakfastQueue.signal();
} finally {
lock.unlock();
}
}
}
小测验
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现?
class AwaitSignal extends ReentrantLock {
private int loopNumber;// 循环次数
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
public void start(Condition first) {
this.lock();
try {
first.signal();
} finally {
this.unlock();
}
}
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
current.await();
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
}
public class Main {
public static void main(String[] args) {
AwaitSignal as = new AwaitSignal(5);
Condition aWaitSet = as.newCondition();
Condition bWaitSet = as.newCondition();
Condition cWaitSet = as.newCondition();
new Thread(() -> {
as.print("a", aWaitSet, bWaitSet);
}).start();
new Thread(() -> {
as.print("b", bWaitSet, cWaitSet);
}).start();
new Thread(() -> {
as.print("c", cWaitSet, aWaitSet);
}).start();
as.start(aWaitSet);
}
}
abcabcabcabcabc
7.5.2、ReentrantReadWriteLock
当读操作远远高于写操作时,这时候使用 读写锁
让 读-读
可以并发,提高性能。
加解读锁
rw.readLock().lock();
rw.readLock().unlock();
加解写锁
rw.writeLock().lock();
rw.writeLock().unlock();
7.5.3、StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用。
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
7.5.4、Semaphore
Semaphore信号量,用来限制能同时访问共享资源的线程上限。
场景:我们现在有6个线程,但是我们每次要求只有2个线程运行,这时候可以使用信号量,来限制能同时访问共享资源的线程上限。
7.5.5、CountDownLatch
CountdownLatch用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。
7.5.6、CyclicBarrier
CyclicBarrier循环栅栏,用来进行线程同步协作,等待线程满足某个计数。
构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行。他可以用于多线程计算数据,最后合并计算结果的场景。CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的。
7.5.7、ThreadLocal
ThreadLocal(线程局部变量),在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。那么在有一种情况之下,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况,ThreadLocal就比较好的解决了这个问题。
7.6线程组的创建
ThreadGroup是Java提供的一种对线程进行分组管理的手段,可以对所有线程以组为单位进行操作,如设置优先级、守护线程等。
class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getThreadGroup() + ":" + Thread.currentThread().getName() + ":" + x);
}
}
}
public class Main {
public static void main(String[] args) {
// ThreadGroup(String name)
ThreadGroup tg = new ThreadGroup("线程组");
// Thread(ThreadGroup group, Runnable target, String name)
Thread t1 = new Thread(tg, new MyRunnable(), "张三");
Thread t2 = new Thread(tg, new MyRunnable(), "李四");
// 获取组名并在控制台输出
System.out.println("t1的线程组名称:" + t1.getThreadGroup().getName());
System.out.println("t2的线程组名称:" + t2.getThreadGroup().getName());
// 表示该组线程是后台线程
tg.setDaemon(true);
// 表示该组线程的优先级5
tg.setMaxPriority(5);
// 启动线程组中的所有线程
t1.start();
t2.start();
// 打印该组活跃线程的信息
tg.list();
}
}