线程-五

[TOC]7

线程补充

线程状态

调用线程有关的方法使线程进入不同的状态。可以通过调用Thread.getState()来获取线程运行的状态。

线程组

可以把线程归属到一个线程组中,线程组可以有线程对象,也可以有线程组,组中还可以有线程。类似嵌套一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SingleEnum {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("thread-1");
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(group, () -> {
System.out.println( ServiceEnum.instance.hashCode());
});
thread.start();
}
System.out.println(group.activeCount()+" 个线程正在运行");
}
}

enum ServiceEnum {
instance;
private SingleEnum singleEnum;
private ServiceEnum(){
singleEnum=new SingleEnum();
}
}

线程组可以有多级关联,也就是父对象有子对象,子对象再创建子对象。

线程组自动归属

如果创建一个线程组却没有指定所属的线程组之后,则线程组会自动归属到当前线程的线程组中。

线程-四

[TOC]

定时器

timer的使用

timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类,

schedule(TimerTask task,Date time)

该方法的作用是在指定日期执行一次某一任务。
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTimer {
private static Timer timer = new Timer();
public static void main(String[] args) throws ParseException {

timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行中。。。"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, new Date(System.currentTimeMillis()+1000));
}

}

上述代码是在一秒之后打印当前时间。
虽然指定了执行时间,都是如果多个任务执行的话会先执行完前面的任务才会执行后面的任务。

schedule(TimerTask task,Date time,long period)

该方法时指定日期之后按照间隔周期无限的执行某一任务。

1
2
3
4
5
6
 timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行中。。。"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}, new Date(System.currentTimeMillis()+1000),1000);
1
2
3
4
5
6
7
运行中。。。2019-09-03 17:37:36
运行中。。。2019-09-03 17:37:36
运行中。。。2019-09-03 17:37:37
运行中。。。2019-09-03 17:37:38
运行中。。。2019-09-03 17:37:39
运行中。。。2019-09-03 17:37:40
运行中。。。2019-09-03 17:37:41

如果传入的时间早于当前时间,则立即执行任务。

schedule(TimerTask task,long delay)

延时delay之后执行一次任务

schedule(TimerTask task,long delay,long period)

延时delay之后执行任务,且每隔period循环一次。

scheduleAtFixedRate

该方法与schedule区别的只有延时,
使用schedule方法:如果执行任务的时间没有被延时,那么下次任务的执行时间参考上一次任务的开始时间来计算。
此方法则参考上一次任务结束的时间。
有延时则没有区别。

TimerTask的cancel

TimerTask的cancel方法时将自身从任务队列中移除

Timer的cancel

和Timertask的cancel不同,Timer的cancel的作用是将全部任务清空。

单例模式与多线程

在多线程的世界里,很多看起来很正常的代码可能会出现不一样的效果。比如单例模式。如果在设计不佳的情况下,就有可能在多线程环境下出现问题。
饿汉模式/立即加载
饿汉模式是在方法调用前就已经创建了实例。

1
2
3
4
5
6
7
public class MyObject{
private static MyObject obj=new MyObject();
private MyObject(){}
public static MyObject getInstance(){
return obj;
}
}

懒汉模式/延迟加载
懒汉模式即在使用的时候加载

1
2
3
4
5
6
7
8
9
10
class MyObject{
private static MyObject obj;
private MyObject(){}
public static MyObject getInstance(){
if (obj!=null){
return obj;
}
return obj=new MyObject();
}
}

但是,这两种模式在多线程下都是线程不安全的。即有可能创建多个实例。
解决方案

synchronized同步方法

在getInstance方法中加入synchronized即可,但是这种方法效率比较低下。

同步代码块

在方法中使用synchronized包裹代码块,这种方案跟同步方法差不多。而且效率也比较低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SingleModeService {
private static SingleModeService service;

private SingleModeService() {
}

public static SingleModeService getInstance() {
if (service == null) {
synchronized (SingleModeService.class) {
service = new SingleModeService();
}
}
return service;
}
}

将判断是否为空放到同步代码外,这样效率会提高一些。

双重检验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SingleModeService {
private static SingleModeService service;

private SingleModeService() {
}

public static SingleModeService getInstance() {
if (service == null) {
synchronized (SingleModeService.class) {
if(service==null){
service = new SingleModeService();
}
}
}
return service;
}
}

使用双重检验锁的原因还是因为前面的同步代码块其实并不安全。主要是因为判空是在同步代码块外面,所以有可能刚刚判断为空,另外一条线程立马就创建了一个实例,这时候已经过了判断空值的行。所以在进入到同步代码块之前还是有可能已经不为空了。

静态内部类

1
2
3
4
5
6
7
8
9
10
public class SingleMode {

private static class SingleModeHandler{
private static final SingleMode single=new SingleMode();
}
private SingleMode(){}
public static SingleMode getInstance(){
return SingleModeHandler.single;
}
}

静态内部类可以解决线程安全的问题。

使用static代码块

静态代码块中的代码在使用类的时候就已经执行了。所以可以使用这个特性来实现单例设计模式。

1
2
3
4
5
6
7
8
9
10
11
public class SingleMode {

private static SingleMode single;
static {
single=new SingleMode();
}
private SingleMode(){}
public static SingleMode getInstance(){
return single;
}
}

使用枚举实现单例

在《Java编程思想》中推荐使用的方式。这也是由于枚举的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SingleEnum {
public static void main(String[] args) {

for (int i = 0; i < 10; i++) {
Thread thread = new Thread( () -> {
System.out.println( ServiceEnum.instance.hashCode());
});
thread.start();
}
}
}

enum ServiceEnum {
instance;
private SingleEnum singleEnum;
private ServiceEnum(){
singleEnum=new SingleEnum();
}
}

输出的结果也是正确的。

线程-三

[TOC]

Lock

ReentrantLock

在JDK1.5的版本中新加入了很多特性。其中就有ReentrantLockReentrantLocksynchronized更灵活。功能也更强大。学习线程的过程中也是不可避免的。
new 一个ReentrantLock 返回一个Lock接口对象。
通过lock加锁。unlock解锁。在lock与unlock之间的代码就是同步区域。

使用Condition实现等待/通知

关键字synchronizedwaitnotify方法相结合可以实现等待通知模式,类ReentrantLock也可以实现同样的功能,但是要借助于Condition对象,它有更好的灵活性,比如实现多路通知功能,也就是说在一个lock对象创建多个Condition实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更灵活。
在notify中,被通知的线程是JVM随机选择的。但使用ReentrantLock结合Condition可以实现选择性通知。
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThreadCondition {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
new Thread(()->{
service.await();
}).start();
Thread.sleep(1000);
service.signal();
}
}
class Service {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void await(){
try{
lock.lock();
System.out.println("A");
condition.await();
System.out.println("A");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void signal(){
try {
lock.lock();
System.out.println("解锁");
condition.signal();
}finally {
lock.unlock();
}
}
}

其中Conditionawait等于Objectwaitsignal等于notify
signalAll等于notifyAll

使用Condition实现选择性通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class ThreadConditionSelect {
public static void main(String[] args) throws InterruptedException {
ThreadConditionSelectService service1 = new ThreadConditionSelectService();
new Thread(() -> {
service1.awaitA();
}).start();
new Thread(() -> {
service1.awaitB();
}).start();
Thread.sleep(500);
service1.signalAll_A();
}
}

class ThreadConditionSelectService {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();

public void awaitA() {
try {
lock.lock();
System.out.println("A:wait");
conditionA.await();
System.out.println("A:run");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void awaitB() {
try {
lock.lock();
System.out.println("B:wait");
conditionB.await();
System.out.println("B:run");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void signalAll_A() {
try {
lock.lock();
System.out.println("A:signalAll");
conditionA.signalAll();
System.out.println("A:signalAll");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void signalAll_B() {
try {
lock.lock();
System.out.println("B:signalAll");
conditionB.signalAll();
System.out.println("B:signalAll");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

在这代码中最终只有A被唤醒。
通过这种方式我们可以唤醒指定种类的线程。

公平锁与非公平锁

锁lock分为公平锁与非公平锁。公平锁表示线程获取锁的顺序是安装线程加锁的顺序来分配的,即FIFO先进先出的顺序。而非公平锁则是抢占。随机获取锁。所以可能会有一些线程一直拿不到锁。
通过创建ReentrantLock的构造函数,传入Boolean值设置锁的类型。
bd0c3c3058122d3cde548384342f72e6.png
通过源码,我们可以知道,在不传入值的情况下默认是非公平锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadGPLock {
public static void main(String[] args) {
ThreadGPLockService gpLockService = new ThreadGPLockService();
for (int i = 0; i < 100; i++) {
new Thread(() -> { System.out.println(Thread.currentThread().getName() + ":运行了");
gpLockService.await();
}).start();
}
}
}

class ThreadGPLockService {
private Lock lock = new ReentrantLock(true);
public void await() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ":获得锁");
} finally {
lock.unlock();
}
}
}

通过结果我们可以看出,当锁是公平锁时,谁先等待那么谁就先获取到锁。而非公平锁则是相互竞争。

ReentrantLock类的方法

getHoldCount()的作用时查询当前线程锁保持锁定的个数。也就是调用lock()方法的次数。
getQueueLength()是返回正在等待获取锁的线程数,表示没有调用await的线程。
getWaitQueueLength()getQueueLength()相反,它表示已经调用了await方法的线程数。
hasQueuedThread(Thread)的作用是查询指定线程释放在等待获取此锁定。
HasQueuedThreads()的作用是查询是否有线程正在等待获取此锁定。
hasWaiters(Condition condition)查询是否有线程正在等待与此锁定有关的condition条件。

lock接口中的方法。

lockInterruptibly()的作用是如果当前线程未被中断,则获取锁定。如果已经被中断则出现异常0

tryLock()的作用是如果锁没被其他线程获取就获得锁。

tryLock(long time, TimeUnit unit)在给定时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。

ReentrantReadWriteLock

ReentrantLock类具有完全排他的效果,在同一时间下只有一个线程在执行lock方法后面的任务,虽然保证了安全性,但是效率相对低下。我们可以使用ReentrantReadWriteLock读写锁,在某些不需要操作实例变量中,完全可以使用读写锁来提升代码运行效率。
读写锁有两个锁,一个是读相关的锁,叫做共享锁,另一个是写操作相关的锁,也称排他锁。写锁与任何锁都互斥。
同一个时刻读锁允许多个线程获取锁,而写锁只允许一个线程获取锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadReadWriteLock {
public static void main(String[] args) {
Service service = new Service();
for (int i = 0; i < 10; i++) {
new Thread(()->{
service.read();
}).start();
}
}
static class Service{
private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
public void read(){
try {
lock.readLock().lock();
System.out.println("获取读锁:"+Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
获取读锁:Thread-0
获取读锁:Thread-3
获取读锁:Thread-5
获取读锁:Thread-2
获取读锁:Thread-1
获取读锁:Thread-6
获取读锁:Thread-4
获取读锁:Thread-8
获取读锁:Thread-7
获取读锁:Thread-9

通过lock.readLock().lock()加一个读锁。
通过lock.writeLock().lock()加一个写锁
如果这个锁是一个ReentrantLock锁,则同一时刻只能允许一个线程运行,所以如果有10个线程。每个线程睡眠0.5秒,十个线程则需要睡5秒。在使用读锁之后,所有线程都可以同时运行。
可见效率要高很多。
写线程与ReentrantLock基本上一致。

线程-二

[TOC]

线程间通信

等待/通知机制

在前面线程方法中我们遇到了wait();notify();方法。接下来我们讲讲这些方法。

wait()

wait方法的作用是将当前执行代码的线程进行等待。但wait并不是Thread的方法而是Object的方法。至于为什么不放在Thread请看这里为什么wait方法定义在Object,而不是Thread。这篇文章讲的很清楚了。
wait方法是将当前线程放入“预执行队列”中。并且在wait所在的代码处停止执行。直到接到通知或中断为止。同时需要注意。在调用wait方法之前。线程需要获得该对象的对象对象级别锁。如果调用wait时没有获取到锁则抛出异常。

wait(long)

带参数的方法表示超过这个时间就会自动唤醒。也就是等待多久。

notify()/notifyAll()

notify();方法恰好与wait相对。当线程使用wait方法等待时可以使用notify()将其唤醒。同理notify也需要在同步方法或同步代码块中调用。
notifyAll()notify()的功能一样。只不过notify()是唤醒一个线程。而notifyAll()是唤醒所有线程。(针对同一个对象的所有线程)。

使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

public class ThreadTest4 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread1 t1 = new Thread1(o);
t1.start();
Thread.sleep(100);
synchronized (o){
System.out.println("开始解锁");
o.notifyAll();
System.out.println("结束解锁");
}


}
}

class Thread1 extends Thread {
private Object lock;
Thread1(Object lock) {
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("wait开始");
lock.wait();
System.out.println("wait结束:");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

在线程run之后进入等待。同时主线程休眠0.5秒。0.5秒之后调用notify唤醒线程。所以在调用wait方法之后wait后面的代码就会停止执行。直到notify执行完毕。当唤醒之后wait后面的代码继续执行
线程之间的状态切换
每个对象都有两个队列。一个是阻塞队列,另一个是就绪队列。当线程被唤醒之后会进入就绪队列。反之则进入阻塞队列。

wait()/notify()/锁

wait执行完后。锁会自动释放。但是执行完notify却不会自动释放锁。从之前的代码中我们知道。代码运行到wait的时候会进行等待。所以wait后面的代码就不执行了。刚开始的时候我以为代码停在这了。所以锁也没释放。其实当wait运行完之后锁就会自动释放。将上面的代码稍加改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

public class ThreadTest4 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread1 t1 = new Thread1(o);
Thread1 t2 = new Thread1(o);
t1.start();
t2.start();
Thread.sleep(100);
synchronized (o){
System.out.println("开始解锁");
o.notifyAll();
System.out.println("结束解锁");
}


}
}

class Thread1 extends Thread {
private Object lock;
Thread1(Object lock) {
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("wait开始");
lock.wait();
System.out.println("wait结束:");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

通过运行的结果表明,在调用wait之后锁就已经被释放掉了。所有线程都要等待notify的同步代码块执行完毕才会继续执行

生产者/消费者模式实现

等待/通知最经典的案例就是“生产者/消费者”模式。

一生产与一消费
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

class P {
private String lock;

P(String lock) {
this.lock = lock;
}

public void setVale() {
try {
synchronized (lock) {
if (!"".equals(ThreadTestCP.value)) {
lock.wait();
}
ThreadTestCP.value = System.currentTimeMillis() + "";
System.out.println("生产者:" + ThreadTestCP.value);
lock.notify();
}
} catch (Exception ignored) {
}
}
}

class C {
private String lock;

C(String lock) {
this.lock = lock;
}

public void getValue() {
try {
synchronized (lock) {
if ("".equals(ThreadTestCP.value)) {
lock.wait();
}
System.out.println("消费者:" + ThreadTestCP.value);
ThreadTestCP.value = "";
lock.notify();
}
} catch (Exception ignored) {
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadTestCP{
public static String value;
public static void main(String[] args){
String lock="aaa";
new Thread(()->{
P p = new P(lock);
while(true){
p.setValue();
}
}).start();
new Thread(()->{
C p = new C(lock);
while(true){
p.setValue();
}
}).start();

}
}

每次生产者生产完成之后唤醒等待线程。然后消费者进行消费。在这里如果出现多个消费者或多个生产者。那么线程就有可能出现假死状态。也就是所有线程都处于等待状态。可以将notify换成notifyAll解决假死状态。
同时这里的notify是没有区分生产者/消费者的。也就是说生产者唤醒的也有可能是生产者。
简单来说就是
消费者查看是否有数据。如果没有就等待。如果有就消费,随后唤醒生产者进行生产。
生产者被唤醒之后进行生产。生产之后唤醒消费者。数据载体可以是任何对象。只要生产者和消费者的数据载体是同一对象。

实战

题目要求: 创建二十个线程。线程分为两种,一种输出“★“,另一种输出”☆“ 两种线程交替运行。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

public class ThreadReplaceRun {
public static void main(String[] args) throws InterruptedException {
Tools tools = new Tools();
for (int i = 0; i < 20; i++) {
new Thread(()->{
tools.MethodA();
}).start();
new Thread(()->{
tools.MethodB();
}).start();
}
}
}

class Tools {
private boolean flag = false;

public void MethodA() {
synchronized (this) {
try {
while (flag) {
wait();
}
System.out.print("★\t");
flag = !flag;
notify();
} catch (Exception ignored) {
}
}
}

public void MethodB() {
synchronized (this) {
try {
while (!flag) {
wait();
}
System.out.print("☆\t");
flag = !flag;
notify();
} catch (Exception ignored) {
}
}
}
}

通过一个Boolean变量使两个线程变成互斥运行。使其其中一个线程进入睡眠。

join方法

很多时候主线程创建并启动子线程,如果子线程中有很多耗时的操作。往往主线程比子线程更早结束。如果在主线程中需要使用到子线程中的数据,这个时候就需要join方法了。
join方法是使所属线程正常运行run方法。当前线程无限制等待。直到所属线程销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadReplaceRun{
public static void main(String agrs) {
Thread t=new Thread(()->{
try{
Thread.sleep(500);
System.out.println("子线程");
}catch(InterruptedException e){}
});
t.start();
t.join();
System.out.println("主线程");
}
}

在执行过程中,子线程会休眠一段时间,在休眠完成之后主线程开始执行。其实join方法的底层也只是使用了wait方法;只是更方便我们使用。
注意 join方法不能与interrupt方法一起使用。

join(long)与sleep(long)的区别

这两者在运行的效果上基本没什么区别。只是咋对待同步的处理上有点区别。由于join底层是使用wait进行等待的,所以具有释放锁的特点。

ThreadLocal

在前面我们都是使用的public static 变量的形式。所有线程都使用同一个public static变量。
ThreadLocal则是为每一个线程提供共享变量的功能。
ThreadLocal可以被视为给线程存放数据的箱子

ThreadLocal线程隔离

我们可以使用一个ThreadLocal类为每一个线程存入数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public class ThreadJoinAndSleep {
public static ThreadLocal tl = new ThreadLocal();

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
tl.set(Thread.currentThread().getName() + "");
System.out.println(Thread.currentThread().getName()+":"+tl.get());
});
t.setName("" + i);
t.start();
}
System.out.println(Thread.currentThread().getName()+":"+tl.get());
}

}

不难发现,每个线程get的都只是那个线程存入的数据。不能获取其他线程的数据。

get为null

在没有进行set之前任何一个线程get返回的都是null。如果想设置一个默认值。我们可以继承ThreadLocal,并实现inittialValue()方法返回默认值

线程-一

[TOC]

线程基础

线程状态

线程的状态又分为五种:

  • New:新建状态
  • Runnable:就绪状态
  • Running:运行状态
  • Blocked:阻塞状态
  • Dead:死亡状态
1
2
3
4
5
6
7
graph TD
New --> Runnable
Runnable-->Running
Blocked-->Runnable
Running-->Runnable
Running-->Blocked
Running-->Dead

线程方法

interrupted()

判断当前线程是否是中断、停止状态,执行后将状态标志清除为false,静态方法。可以直接调用

isInterrupted()

判断当前线程是否是中断、停止状态。但不清除状态标志。非静态方法。

interrupt()

将状态标记为中断,用于线程退出。在run方法中使用interrupted判断线程状态。如果返回truereturn或抛出异常。

yield()

将Running状态转变为Runnable状态。把线程CPU让给其他线程,让出时间不确定,有可能上一秒让出下一秒又获取到。

sleep()

使线程休眠多少毫秒,参数为long ms

join()

join方法的作用是父线程等待子线程执行完成后再执行。换句话说就是将异步执行的线程合并为同步的线程。

wait/notify/notifyAll方法

这些方法并不是Thread类中的。而是Object类的方法。不过这些也是学习线程中不可或缺的。

wait()

wait()方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程。此方法需要与锁一起使用。

notify

与使用wait的同一对象使用该方法即可唤醒处于等待的线程。

notifyAll

notify方法差不多,只不过notify只唤醒当前线程。而notifyAll则唤醒所有线程。

线程优先级

在线程中可以通过setPriority(int) 来设置线程优先级。子类的优先级与父类的优先级一致。
线程优先级从低到高为1~10。超过则抛出IllegalArgumentException()异常
且线程优先级不代表一定会按照顺序执行。

守护线程

在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
一旦用户线程全都执行完毕之后,守护线程也会结束。没有用户线程时守护进程将自动销毁。
线程可以在执行start()方法之前执行setDaemon(true) 变成守护进程。

对象及变量并发访问

当多个线程对一个对象实例或者一个对象实例变量访问的时候就有可能出现线程安全。

方法内的变量为线程安全

所有线程内部的私有变量都不会引起线程安全

synchronized

synchronized所声明的方法为线程安全。
synchronized 使用:
synchronized可以在方法签名中声明。也可以声明代码块。

1
2
3
4
5
6
public synchronized void mothed(){
...
}
synchronized(Obj){
...
}

最好不要使用String,或其他基本数据类型作为synchronized所修饰的对象。因为

1
2
String a1="aa";
String a2="aa";

由于常量池的原因。a1=a2。所以在synchronized(String)中会被当成同一个对象。
使用synchronized之后,异步方法会进行排队。可以看作原子级的方法。
只有共享变量/实例才需要同步。如果不是共享变量或实例则不需要同步。

synchronized 重入

synchronized声明的代码块,方法来说都具有重入的特性,也就是说synchronized代码块或方法可以调用同一个对象的synchronized而不会产生死锁。

synchronized 同步代码块

使用synchronized声明一个同步代码块比直接声明一个方法性能要好很多。同时使用一个对象实例比this效率要高一些。使用synchronized(非this)代码块中的程序与其他同步方法是异步的。因为他锁的对象不是this,如果需要同步的话需要锁住的对象是同一个。
注意:同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行顺序。所以非常容易产生“脏读”的问题。
也就是说synchronized修饰方法时,锁住的对象是this。所以synchronized修饰的方法与synchronized(非this)的代码块之间非常容易出问题。

synchronized静态同步

synchronized可以用到静态方法上,这样写是对当前*.java文件对应的Class类进行加锁。synchronized加到静态方法上是对Class加锁。synchronized加到非静态方法上是给对象加锁。如果有一个静态方法。同时有两个实例调用此方法。则该方法为同步方法。
synchronized(Object.class)等于synchronized声明静态方法。

锁对象改变

在同步代码块中如果锁对象发生了改变,会发生什么呢?锁对象还有用吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

public class test {
public static void main(String[] args) {
Test1 test1 = new Test1();
new Thread(() -> {
try {
test1.ts(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A1").start();
new Thread(() -> {
try {
test1.ts(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A2").start();
}
}

class Test1 {
private String lock = "a1";

public void ts(String name) throws InterruptedException {
synchronized (lock) {
System.out.println(name + ":start");
lock = "a2";
Thread.sleep(1000);
System.out.println(name + ":end");
}
}
}

真实情况是:一旦锁发生了改变,且在改变之后又有新的线程进来。那么这两个线程的锁对象就不相等。就会变成异步方法。同时可能会产生“脏读”的情况。

1
2
3
4
A1:start
A2:start
A1:end
A2:end

提示: 只要对象不变。就算对象的属性变了,运行的结果也是同步。

volatile

volatile是为了确保公共堆栈中的变量与线程私有栈中的变量保持一致。使用了volatile之后会从公共堆栈取值。
如图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

public class ThreadTest2 {
public static void main(String[] args) throws InterruptedException {
volatileTest test = new volatileTest();
test.start();
Thread.sleep(1000);
test.setFlag(false);
}
}

class volatileTest extends Thread {
private boolean flag = true;
private long count = 0;

public void setFlag(boolean flag) {
this.flag = flag;
}

@Override
public void run() {
System.out.println("进入run");
while (flag) {
count++;
}
System.out.println("退出run:count = " + count);
}
}

如果线程没有及时读到最新的值。那么就可能会出现死循环。

他会一直卡在while循环中。因为在公共堆栈中已经修改了flag的值。但是在线程私有栈中却没有更新flag的值
加了volatile之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

public class ThreadTest2 {
public static void main(String[] args) throws InterruptedException {
volatileTest test = new volatileTest();
test.start();
Thread.sleep(1000);
test.setFlag(false);
}
}

class volatileTest extends Thread {
private volatile boolean flag = true;
private long count = 0;

public void setFlag(boolean flag) {
this.flag = flag;
}

@Override
public void run() {
System.out.println("进入run");
while (flag) {
count++;
}
System.out.println("退出run:count = " + count);
}
}

运行结果:

1
2
进入run
退出run:count = 744998007
volatile与synchronized

乍一看volatile似乎可以解决多线程下的“脏读”问题了。但是其实上并不是。

  • volatile是线程同步的轻量级实现。所以volatile的性能比synchronized要好。但是随着JDK版本的提升。synchronized效率也的到了加强。
  • 多线程访问volatile不会发生阻塞。synchronized会发生阻塞。
  • volatile能保证数据可见性。但是不能保证原子性。而synchronized可以保证原子性。也可以间接保证可见性。因为他会将私有内存与公共内存中的数据做同步。
  • volatile解决的是变量在多个线程中的可见性。而synchronized解决的是多个线程之间访问资源的同步性。
  • 线程安全包含原子性和可见性两个方面。Java的同步机制都是围绕这两个方面来确保线程安全的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadTest3 {    
public static void main(String[] args) {
for (int i = 0; i < 100; i++) { 
new volaitleTest().start();       
}
}
}
class volaitleTest extends Thread
private static int count; 
@Override   
public void run()
for (int i = 0; i < 100; i++) { 
count++;   
}


System.out.println("count:" + count);   
}
}

在所有的结果中都没有count=10000的这条结果。这个问题是因为count++并不是一个原子性操作。
解决: 有一点需要注意。也就是countstatic所修饰的。而且在main方法中是new了100个线程。所以我们在使用synchronized的时候锁对象一定是一个Class
如将synchronized添加到static方法中。或者synchronized代码块中的锁对象是一个当前类的Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class ThreadTest3 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new volaitleTest().start();
}
}
}
class volaitleTest extends Thread {
private static int count = 0;

@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (volaitleTest.class) {
count++;
}
}
System.out.println("count:" + count);
}
}

从结果中可以看出虽然顺序是随机的。但是一定有一条数据是count等于10000;
方案二 使用原子类
在前面我们使用了synchronized,这次我们不用synchronized而使用原子类。
原子类可以将i++这样的操作当成一个原子操作。而不使用锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class ThreadTest3 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new volaitleTest().start();
}
}
}

class volaitleTest extends Thread {
private static AtomicInteger count = new AtomicInteger(0);

@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (volaitleTest.class) {
count.incrementAndGet();
}
}
System.out.println("count:" + count);
}
}

可以看到,最终结果也有10000。针对与i++这样的操作可以使用原子类来做到线程安全。
还有个问题 虽然最终的结果已经是10000了,但是他不是顺序的。这是因为方法与方法之间不是同步的。将代码稍微改一下:将输出语句添加到同步块中,这样结果就是顺序的了。

使用一个变量表示多种状态

枚举与多状态

前言

我们通常会使用一个枚举表示状态,如

1
2
3
4
5
enum State{
A,
B,
C,
}

我们可以声明一个枚举变量表示一个状态
但是: 如果我要表示多个状态怎么办。这种方式只能解决一个状态的问题。

二进制的位运算

枚举的多个状态可以通过二进制的位运算来实现。
原理是什么

1
int state=0;//表示一个状态
  1. 使用 | 将每一个状态添加到state里面。如1的二进制是0001,2的二进制为0010。包含两个状态的值为0011。
  2. 使用 & 来判断state是否包含此状态,如1000&0101=1000,因为我们是通过每一位是否包含1来判断是否包含这个状态。所以每一个状态必须满足2^n。
    我们将枚举的每一个值写作2^n,这样每一个值在二进制下只有一位为1,其余为0。

    实现

    枚举声明如下:

    1
    2
    3
    4
    5
    enum State{
    A=0,
    B=1<<0,
    C=1<<1,
    }
    1
    private State state  =0;//状态

    添加状态

    1
    2
    3
    4

    private addState(State state) {
    this.state = this.state | state;
    }

    判断是否包含状态

    1
    2
    3
    private boolean includeState(State state) {
    return (this.state & state) == state;
    }

    删除状态

    删除状态需要先判断一下是否存在此状态。然后通过添加状态的取反就可以将状态删除。
    1
    2
    3
    private removeState(State state) {
    this.state = (~state)&this.state;
    }