java并发(1)基本的线程机制

线程池、线程优先级、让步、两种互斥同步方法、线程安全、线程中断等。

1.Executor:

Executor(执行器)将为你管理Thread对象,Executor在客户端和任务执行之间提供了一个间接层。ExecutorService(具有服务生命周期的Executor,例如关闭)知道如何构建恰当的上下文来执行Runnable对象。

使用如下:

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
public class Test1 implements Runnable {

private static int count = 0;

private final int id = count++;

public Test1() {
System.out.println();
}

@Override
public void run() {
System.out.println(id + " run() method is run");
Thread.yield();
}

public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
// ExecutorService exec = Executors.newFixedThreadPool(3);
// ExecutorService exec = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
exec.execute(new Test1());
}
exec.shutdown();
}
}

/* Output:
0 run() method is run
1 run() method is run
2 run() method is run
3 run() method is run
4 run() method is run
*/

对shutdown()方法的调用可以防止新的任务被提交给这个Executor,其中:

  • CachedThreadPool 将为每个任务都创建一个线程, 它是合理的Executor的首选。
  • FixedThreadPool将一次性预先执行代价较高昂的线程分配,因而可以显示线程数量,这可以节省时间,你不用为每个任务都固定地付出创建线程的开销,在线程池中,现有线程在可能的情况下都会被自动复用。
  • SingleThreadExecutor是线程数量为1的FixedThreadPool,如果向SingleThreadExecutor提交了多个任务,这些任务将排队。

2.优先级:

调度器更倾向于让让优先级高的线程先执行。但是并不是说优先级高的线程执行时优先级低的线程得不到执行,也就是说,优先级不会导致死锁,优先级低的线程仅仅是执行的频率较低。在绝大多时间里,所有线程都应该以默认的优先级运行,试图操作线程的优先级通常是一种错误。

可以使用getPriority()方法获得线程的优先级,使用setPriority(n)方法来设定线程优先级。

1
2
3
4
public void run() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
System.out.println("hello thread! "+Thread.currentThread().getPriority());
}

以上Thread.currentThread()可以获得对驱动该任务的Thread对象的引用,设置线程的优先级一般在run()的开头不封设定,在构造器中设定它们不会有任何好处,因为Executor在此刻还没有开始执行任务

3.让步:

当任务已经完成的差不多了觉得可以让其他线程工作了的时候可以给线程调度机制一个暗示:你的工作已经做得差不多了,可以让其他线程使用cpu了,这个暗示将通过调用yield()方法(不过这只是一个暗示,没有任何机制保证它将被采纳)。当调用yield()时你也是建议具有相同优先级的其他线程可以运行。

4.后台线程:

所谓后台线程,是指在程序运行时在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的一部份。在线程启动之前调用setDaemon()方法可以将线程设置为后台线程,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程,后台线程在不执行finally子句的时候就会终止其run()方法

1
2
3
4
5
for(int i = 0; i<10; i++) {
Thread daemon = new Thread(new Test1);
daemon.setDaemin(true);
daemon.start();
}

5.中断:

一个线程在执行完毕之后(run方法执行完)会自动结束,如果在运行过程中发生异常也会提前结束,线程中断可以通过调用intercept()来实现,再介绍interrupt()方法之前先介绍一下InterruptedException异常,该异常会在调用线程interrupt()方法的时候抛出,通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

Thread.sleep()方法会导致线程阻塞,所以该方法会抛出InterruptedException异常,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class InterruptExample {
private static class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

如果在sleep期间调用该线程的interrupt()方法就会抛出InterruptException异常,并执行catch块中的内容。如果run方法中没有阻塞或等待状态,无法抛出InterruptException异常,调用interrupt()方法之后不会抛出异常,但是会修改线程的中断状态,可以在线程中通过interrupted()方法获取线程的中断状态,并主动结束线程,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InterruptExample {
private static class MyThread extends Thread {
@Override
public void run() {
while (!interrupted()) {
// do something...
}
System.out.println("Thread end");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
thread.interrupt();
}
}

每次while循环都会检查线程的中断状态,当调用该线程的interrupt()方法之后,线程的中断状态被标记为true,在线程执行下一次while循环的时候调用interrupted()方法返回线程的中断状态为true,退出while循环结束线程。

Executor 的中断操作:
当调用Executor的shutdownNow()方法时会调用每个线程的interrupt()方法来中断线程。

5.线程互斥同步:

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock,在jdk8对synchronized进行了优化,现在synchronized和ReentrantLock在性能上持平,但是ReentrantLock添加了很多高级功能,所以现在不以性能来作为选择ReentrantLock的条件,只有在需要使用到ReentrantLock的高级功能的时候才选择ReentrantLock,否则默认都选择synchronized。

synchronized:
synchronized是由JVM实现的,synchronized可以锁住对象和类而不是锁住代码

锁住一个对象:

1
2
3
4
5
public void func () {
synchronized (this) {
// ...
}
}

锁住一个类:

1
2
3
4
5
public void func () {
synchronized (SynchronizedExample.class) {
// ...
}
}

当使用synchronized锁住一个对象的时候,会修改该对象的对象头(一个实例对象包括对象头、实例数据、填充数据三块)信息,对象头中包含了hashcode、分代年龄、锁标志等信息,所以当某个线程获得该对象的对象锁时,对象头信息的锁标志被标识为锁定状态,其他线程想要执行这段代码时发现对象处于锁定状态,所以只能排队等候,直到当前线程执行完同步代码锁被释放,才轮到下一个线程持有对象锁,执行同步代码,所以当一个对象有多个同步方法时,某个线程执行方法一获得了对象锁,其他线程执行方法二依然会等待锁的释放。

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
public class SynchronizedExample {

public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}

public void func2() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}

public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(e1::func1);
executorService.execute(e1::func2);
}
}
//
执行结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
Process finished with exit code 0

当使用synchronized同步一个类的时候,该类的所有实例对象都会被同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SynchronizedExample {

public void func1() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}

}

当使用synchronized同步一个静态方法的时候和同步类相同,也是作用于整个类。

ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LockExample {

private Lock lock = new ReentrantLock();

public void func() {
lock.lock(); //获取锁
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}

ReentrantLock多了以下高级功能:

  • 等待可中断
    当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

  • 可实现公平锁
    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

  • 锁绑定多个条件
    一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。