之前写的都乱糟糟的,现在也需要重新记忆一遍。所以重新整理一下JUC包。
多线程及其优缺点
什么是线程
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。(wiki百科)
创建线程的三种方式
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
//1、继承Thread方式
Thread thread1 = new Thread(){
@Override
public void run() {
System.out.println("thread1 start");
}
};
thread1.start();
//2、实现Runnable接口
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2 start");
}
});
thread2.start();
//3、实现Callable接口
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Callable() {
@Override
public String call() throws Exception {
return "future start";
}
});
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
在jdk8之后用lambda表达式转换一下
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
//1、继承Thread方式
Thread thread1 = new Thread(() -> System.out.println("thread1 start"));
thread1.start();
//2、实现Runnable接口
Thread thread2 = new Thread(() -> System.out.println("thread2 start"));
thread2.start();
//3、实现Callable接口
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(() -> "future start");
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
简化了一点,但是更多是有点懵,lambda为什么会简化方法,->
是怎么找到对应的方法,下次在研究。
为什么要用多线程
早期的CPU是单核的,为了提升计算能力,将多个计算单元整合到一起。形成了多核CPU。多线程就是为了将多核CPU发挥到极致,一边提高性能。
多线程缺点呢
上面说了多线程的有点是:为了提高计算性能。那么一定会提高?
答案是不一定的。有时候多线程不一定比单线程计算快。引入《java并发编程的艺术》上第一个例子
public class ConcurrencyTest {
/** 执行次数 */
private static final long count = 10000l;
public static void main(String[] args) throws InterruptedException {
//并发计算
concurrency();
//单线程计算
serial();
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
System.out.println(a);
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("concurrency :" + time + "ms,b=" + b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
}
}
结果为
50000
concurrency :22ms,b=-10000
serial:0ms,b=-10000,a=50000
而且多线程会带来额外的开销
- 上下文切换
- 线程安全
上下文切换
时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。
- 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
- CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
线程安全的问题
多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况
同样引入《java并发编程的艺术》的一个例子
public class DeadLockDemo {
private static String resource_a = "A";
private static String resource_b = "B";
public static void main(String[] args) {
deadLock();
}
public static void deadLock() {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_a) {
System.out.println("get resource a");
try {
Thread.sleep(3000);
synchronized (resource_b) {
System.out.println("get resource b");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_b) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get resource b");
synchronized (resource_a) {
System.out.println("get resource a");
}
}
}
});
threadA.start();
threadB.start();
}
}
然后通过jps
查看,找个这个类的id
然后通过jstack id
来查看
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000016074808 (object 0x00000000e0b89280, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000016075ca8 (object 0x00000000e0b892b0, a java.lang.String),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1" #11 prio=5 os_prio=0 tid=0x00000000175ba800 nid=0x232c waiting for monitor entry [0x000000001889f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLockDemo$2.run(DeadLockDemo.java:37)
- waiting to lock <0x00000000e0b89280> (a java.lang.String)
- locked <0x00000000e0b892b0> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread-0" #10 prio=5 os_prio=0 tid=0x00000000175b7800 nid=0x234c waiting for monitor entry [0x000000001861f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLockDemo$1.run(DeadLockDemo.java:18)
- waiting to lock <0x00000000e0b892b0> (a java.lang.String)
- locked <0x00000000e0b89280> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
两个线程相互等待,仔细看上面的waiting to lock 和locked两个对象。是相互的。造成死锁。
造成死锁的原因和解决方案
死锁:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
造成死锁的原因是:
- 因为系统资源不足。
- 进程运行推进的顺序不合适。
- 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则
就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
那么死锁的必要条件是:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是 死锁的必要条件 ,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。
线程的状态
线程有6种状态
- NEW:新建,线程被构建,但是还没有start()
- RUNNABLE:运行,java中将就绪和运行统称为运行中
- BLOCKED:阻塞,线程阻塞于锁
- WAITING:等待,表示线程进入等待状态,需要其他线程的特定动作(通知或中断)
- TIMED_WAITING:带超时的等待,可以在指定的时间内自动返还
- TERMINATED:终止,表示线程已经执行完毕
线程创建之后调用
start()
方法开始运行。
当调用
wait(),join()
,LockSupport.lock()
方法线程会进入到WAITING
状态,而同样的wait(long timeout)
,sleep(long), join(long)
, LockSupport.parkNanos()
, LockSupport.parkUtil()
增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING
状态,当超时等待时间到达后,线程会切换到Runable
的状态,另外当WAITING
和TIMED _WAITING
状态时可以通过Object.notify()
,Object.notifyAll()
方法使线程转换到Runable
状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED
阻塞状态,当线程获取锁时,线程进入到Runable
状态。线程运行结束后,线程进入到TERMINATED
状态,状态转换可以说是线程的生命周期。
注意
当线程进入到
synchronized方法
或者synchronized代码块
时,线程切换到的是BLOCKED
状态.
而使用
java.util.concurrent.locks
下lock
进行加锁的时候线程切换的是WAITING
或者TIMED_WAITING
状态,因为lock会调用LockSupport的方法。
线程状态的操作
interrupted()
中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。
其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用 isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。
另外,同样可以调用Thread的静态方法 interrupted()对当前线程进行中断操作,该方法会清除中断标志位。
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
//sleepThread睡眠1000ms
final Thread sleepThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
};
//busyThread一直执行死循环
Thread busyThread = new Thread() {
@Override
public void run() {
while (true) ;
}
};
sleepThread.start();
busyThread.start();
sleepThread.interrupt();
busyThread.interrupt();
while (sleepThread.isInterrupted()) ;
System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
}
}
运行结果是:
对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。
join()
join方法可以看做是线程间协作的一种方式。
如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。
public class JoinDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 1; i <= 5; i++) {
Thread curThread = new JoinThread(previousThread);
curThread.start();
previousThread = curThread;
}
}
static class JoinThread extends Thread {
private Thread thread;
public JoinThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
//join
thread.join();
System.out.println(thread.getName() + " terminated.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
如果注释了上面的thread.join();
每个线程都会等待前一个线程结束才会继续运行。
sleep() VS wait()
两者主要的区别:
- sleep()方法是Thread的静态方法,而wait是Object实例方法
-
wait()
方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()
方法没有这个限制可以在任何地方种使用。另外,wait()
方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()
方法只是会让出CPU并不会释放掉对象锁; -
sleep()
方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()
方法必须等待Object.notift/Object.notifyAll
通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
守护线程Daemon
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。下面以一个简单的例子来表述Daemon线程的使用。
public class DaemonDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println("i am alive");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally block");
}
}
}
});
//设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
//确保main线程结束前能给daemonThread能够分到时间片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
守护线程应该先于
start()
方法之前。如果在之后,但是该线程还是会执行,只不过会当做正常的用户线程执行。
其他的一些概念
同步和异步
同步和异步通常用来形容一次方法调用。
同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。
而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。
并发与并行
并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。
实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。
阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响。
比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞。
而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。