长大后想做什么?做回小孩!

0%

Java多线程

进程和线程的概念:

这不是一个复杂的概念,网上的解释有很多,引用百度到的一个解释。

转载自博客园

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;或者更专业化来说:进程是指程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。进程——资源分配的最小单位,线程——程序执行的最小单位。

线程进程的区别体现在4个方面:

  1. 因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。

  2. 体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。。

  3. 体现在CPU系统上面,线程使得CPU系统更加有效,因为操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

  4. 体现在程序结构上,举一个简明易懂的列子:当我们使用进程的时候,我们不自主的使用if else嵌套来判断pid,使得程序结构繁琐,但是当我们使用线程的时候,基本上可以甩掉它,当然程序内部执行功能单元需要使用的时候还是要使用,所以线程对程序结构的改善有很大帮助。

什么情况下使用进程个线程:

  1. 需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的

  2. 线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应

  3. 因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程

  4. 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求

  5. 需要更稳定安全时,适合选择进程;需要速度时,选择线程更好

     因为我的项目中需要对数据段的数据共享,可以被多个程序所修改,所以使用线程来完成此操作,无需加入复杂的通信机制,使用进程需要添加复杂的通信机制实现数据段的共享,增加了我的代码的繁琐,而且使用线程开销小,项目运行的速度快,效率高。
    
    如果只用进程的话,虽然安全性高,但是对代码的简洁性不好,程序结构繁琐,开销比较大,还需要加入复杂的通信机制,会使得我的项目代码量大大增加,切换速度会变的很慢,执行效率降低不少。。。

进程和线程的关系:

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。

  3. 处理机分给线程,即真正在处理机上运行的是线程。

  4. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。


引子:

先看一段简单的代码:

1
2
3
4
5
6
7
public class MyThread{
public void run(){
while (true){
System.out.println("MyThread的run方法在运行");
}
}
}
1
2
3
4
5
6
7
8
9
public class Demo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.run();
while (true){
System.out.println("Main方法的循环");
}
}
}

运行结果可想而知,当程序执行到myThread.run();时就会一直打印”MyThread的run方法在运行”,导致main方法中的”Main方法的循环”永远无法执行打印。

这种情况就是因为该程序是一个单线程程序,如果希望两个while循环中的打印语句都能够并发执行,就需要多线程。


创建新线程和启动:

Java中可以通过继承Thread类重写run()方法实现多线程,Tread提供了一个start()方法,用于启动新线程:

1
2
3
4
5
6
7
public class MyThread extends Thread{
public void run(){
while (true){
System.out.println("MyThread的run方法在运行");
}
}
}
1
2
3
4
5
6
7
8
9
public class Demo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();//启动新线程
while (true){
System.out.println("Main方法的循环");
}
}
}

运行程序,两个while循环中的字符串不断地交替打印。大概是下图这个样子:

uAzL6I.png

虽然继承Thread类实现了多线程,但是这种方式有一定的局限性:因为Java只支持单继承(之前写的Java继承策略中聊过),一个类一旦继承了某个父类就无法再继承Thread类。

虽然可以通过内部类继承Thread类的方法解决这个问题,但是显然这不会是一个好的方法。于是,Java还提供了实现Runnable接口创建多线程的方法,Thread类提供了一个构造方法Thread(Runnable target)。其中,Runnable是一个接口,他只有一个run()方法:

1
2
3
4
@FunctionalInterface
public interface Runnable {
void run();
}

当通过Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。

1
2
3
4
5
6
7
public class MyThread implements Runnable{
public void run(){
while (true){
System.out.println("MyThread的run方法在运行");
}
}
}
1
2
3
4
5
6
7
8
9
10
public class Demo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
while (true){
System.out.println("Main方法的循环");
}
}
}

也可以实现和继承Thread类一样的效果,Runnable再应用时经常用匿名内部类的方式创建实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true){
System.out.println("MyThread的run方法在运行");
}
});//Lambda表达式创建Runnable的匿名内部类传参
thread.start();
while (true){
System.out.println("Main方法的循环");
}
}
}

还不会流式语法的朋友可以参考之前的文章:Java8特性(一)


继承Thread类和实现Runnable接口的区别:

还是先看个例子:假设售票厅有4个窗口可发售某日某次列车的票100张,这时100张车票可以看作是共享资源,四个售票窗口需要创建4个线程。

先用继承Thread类方式创建多线程:

1
2
3
4
5
6
7
8
9
10
11
12
public class TicketWindow extends Thread{
private int tickets = 100;
public void run(){
while (true){
if (tickets>0){
Thread th = Thread.currentThread();
String th_name = th.getName();
System.out.println(th_name+"正在发售第"+ tickets-- +"张票!");
}
}
}
}
1
2
3
4
5
6
7
8
public class Demo {
public static void main(String[] args) {
new TicketWindow().start();
new TicketWindow().start();
new TicketWindow().start();
new TicketWindow().start();
}
}

运行结果:Thread-0、Thread-1、Thread-2、Thread-3分别都卖了100张票,明显不符合场景要求。出现这种情况的原因是因为创建了4个TicketWindow就等于创建了四个程序,每个程序都有100张票,每个线程独立地处理各自的资源。

用实现Runnable接口方式创建多线程:

1
2
3
4
5
6
7
8
9
10
11
12
public class TicketWindow implements Runnable{
private int tickets = 100;
public void run(){
while (true){
if (tickets>0){
Thread th = Thread.currentThread();//获取当前线程
String th_name = th.getName();//获取线程的名字
System.out.println(th_name+"正在发售第"+ tickets-- +"张票!");
}
}
}
}
1
2
3
4
5
6
7
8
9
public class Demo {
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow();
new Thread(ticketWindow).start();
new Thread(ticketWindow).start();
new Thread(ticketWindow).start();
new Thread(ticketWindow).start();
}
}

只创建了一个TicketWindow对象,然后创建了4个线程,每个线程都调用这个TicketWindow对象的run()方法,这样就可以确保4个线程访问的是同一个tickets变量,共享100张票。

通过上面这个例子,总结实现Runnable接口相对于继承Thread类来说,有如下优点:

  1. 适合多个相同程序代码的线程去处理同一个资源的情况,把线程与程序代码、数据有效分离,很好地体现了面向对象的设计思想。
  2. 可以避免由于Java的单继承带来的局限性。

事实上大部分的多线程应用都会采取实现Runnable接口的方法。


线程的生命周期及状态转换:

在Java中,任何对象都有生命周期,线程也不例外。当Thread对象创建完成时,线程的声明周期便开始了。当run()方法中的代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。线程整个生命周期分为5个阶段:新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、和死亡状态(Terminated)。如下图所示:

uEebad.png

1.新建状态(New)

创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。可以通过调用start方法进入就绪状态(runnable).

注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

2.就绪状态(Runnable)

当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度

尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的。

提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

3.运行状态(Running)

如果处于就绪状态的线程获得CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,他可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。

4.阻塞状态(Blocked)

一个正在执行的线程在某些特殊情况下,如被人为挂起执行耗时的输入/输出操作时,会让出CPU的使用权暂时终止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

下面列举一下线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态:

  • 当线程视图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态。如果想从阻塞状态进入就绪状态必须获取到其他线程所持有的锁。
  • 当线程调用了一个阻塞式IO方法时,也会使线程进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。
  • 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪队列就需要使用notify()方法唤醒该线程。
  • 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态。这种情况下,只需要等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
  • 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态。在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

需要注意的是:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池,等待系统的调度。

5.死亡状态(Terminated)

当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。


线程的调度:

在计算机中,线程调度有两种模式,分别是分时调度模型抢占式调度模型。所谓分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当他失去了CPU的使用权后,再随机选择其他线程获取CPU的使用权。JVM默认采用抢占式调度模型,通常情况下程序员不需要去考虑它,但在某些特定需求下需要改变这种模式,由程序自己来控制CPU的调度。

线程的优先级:

如果要对线程进行调度,最直接的方法就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,反之,机会越小。线程的优先级有1~10的整数来表示,数字越大优先级越高。除了直接使用数字表示线程的优先级,还可以使用Thread中提供的三个静态常量表示线程的优先级:

1
2
3
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

程序运行时,处于就绪状态的每个线程都有自己的优先级,例如:main线程具有普通优先级。然而线程的优先级不是固定不变的,可以通过setPriority(int newPriority)方法进行设置,方法参数接受1~10整数或上述的静态常量:

1
2
3
4
5
6
7
public class Task extends Thread{
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"正在打印"+i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) {
Thread minPriority = new Thread(new Task(),"低优先级的线程");
Thread normPriority = new Thread(new Task(),"中优先级的线程");
Thread maxPriority = new Thread(new Task(),"高优先级的线程");
minPriority.setPriority(Thread.MIN_PRIORITY);
normPriority.setPriority(Thread.NORM_PRIORITY);
maxPriority.setPriority(Thread.MAX_PRIORITY);
minPriority.start();
normPriority.start();
maxPriority.start();
}
}

如果没有设置优先级,打印顺序和线程启动顺序一样。设置优先级之后,打印顺序按照线程优先级从高到低。getPriority()方法可以获取线程的优先级。

需要注意:不同的操作系统对优先级的支持是不一样的,不会与Java中线程优先级一一对应。因此,再设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能讲优先级作为一种提高程序效率的手段。

线程休眠:

如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,可以使用静态方法sleep(long millis),该方法可以是当前正在执行的线程暂停一顿时间,在指定时间内进入阻塞状态。

sleep(long millis)方法声明会抛出InterruptException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Task extends Thread{
public void run(){
for(int i=0;i<10;i++){
try {
if (i==3){
Thread.sleep(2000);
}else {
Thread.sleep(500);
}
System.out.println(Thread.currentThread().getName()+"正在打印"+i);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) throws InterruptedException {
new Thread(new Task()).start();
for(int i=0;i<10;i++){
if (i==3){
Thread.sleep(2000);
}else {
Thread.sleep(500);
}
System.out.println(Thread.currentThread().getName()+"正在打印"+i);
}
}
}

因为两个线程存在休眠,每次一个线程休眠时,另一个线程就会获得执行,所以最后效果是main线程和thread-0线程交替执行打印。

sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠结束后,线程就会返回到就绪状态,而不是立即开始运行。

线程让步:

如果希望正在执行的线程将CPU资源让给其他线程执行。可以使用yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前运行的线程暂停。区别:yield()方法不会阻塞该线程,他只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

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

public YieldThread(String name){
super(name);
}
public void run(){
for (int i= 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印"+i);
if (i==3){
System.out.println("线程让步:");
Thread.yield();
}
}
}
}
1
2
3
4
5
6
7
8
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new YieldThread("线程A");
Thread t2 = new YieldThread("线程B");
t1.start();
t2.start();
}
}

t1和t2线程优先级相同,开始是分别都有执行,当两个线程的循环遍历等于3时,就会线程让步,另一个线程就会获得执行。

线程插队:

Thread类提供了一个join()方法来实现插队功能。当某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后他才会继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JoinThread extends Thread{

public void run(){
for (int i= 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印:"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new JoinThread(),"线程一");
t.start();
for (int i=1;i<10;i++){
System.out.println(Thread.currentThread().getName()+"打印:"+i);
if (i==2){
t.join();
}
Thread.sleep(500);
}
}
}

main和线程一都有sleep()方法,应该交替执行。但是main线程中在i==2时执行了线程一的join()方法,main就会等待线程一死亡后再继续执行。

join()有三个重载方法:

1
2
3
4
5
6
void join()      
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度

后台(守护)线程:

守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数超时时间状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:

  • 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。

  • Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

setDaemon方法的详细说明:

1
2
3
4
5
6
7
8
9
10
//将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。    
//该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。
//这可能抛出 SecurityException(在当前线程中)。
public final void setDaemon(boolean on) {//on - 如果为 true,则将该线程标记为守护线程。
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
  • 如果该线程处于活动状态。抛出:IllegalThreadStateException 异常。

  • 如果当前线程无法修改该线程。在当前线程中抛出:SecurityException异常。

注:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。

正确的结束线程:

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!!!

想要安全有效的结束一个线程,可以使用下面的方法:

  • 正常执行完run方法,然后结束掉。

  • 控制循环条件和判断条件的标识符来结束掉线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MyThread extends Thread {  
    int i=0;
    boolean next=true;
    @Override
    public void run() {
    while (next) {
    if(i==10)
    next=false;
    i++;
    System.out.println(i);
    }
    }
    }

线程同步:

多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。

最开始”继承Thread类和实现Runnable接口的区别“讲解部分举了一个售票窗口售票的例子,改为模拟四个窗口出售10张票,并在每次售票后要sleep()10毫秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TicketWindow implements Runnable{
private int tickets=10;

public void run(){
while (tickets>0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买票:"+tickets--);
}
}
}
1
2
3
4
5
6
7
8
9
public class Demo {
public static void main(String[] args) throws InterruptedException {
TicketWindow ticketWindow = new TicketWindow();
new Thread(ticketWindow,"窗口1").start();
new Thread(ticketWindow,"窗口2").start();
new Thread(ticketWindow,"窗口3").start();
new Thread(ticketWindow,"窗口4").start();
}
}

除了预期的10号到1号的票,竟然出现了0号、-1号、-2号车票,这种现象是不应该的,因为售票中做了判断,只有票号大于0时才会售票。

之所以出现了0、-1、-2的情况是因为线程由延迟(sleep()模拟了线程延迟),假如线程1(窗口1)出售1号票,对票号进行了判断后,进入while循环,在售票前通过sleep()方法让线程休眠,这时线程2(窗口2)会进行售票,因为此时的票号仍为1,因此线程2也会进入循环。同理,4个线程都会进入while循环,休眠结束之后,4个线程都会进行售票(tickets–),这样就相当于将票号减了4次,导致出现了0、-1这样的票号。

同步代码块:

线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决这样的线程安全问题,必须得保证处理共享资源的代码在任何时刻只能有一个线程访问。

为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这个代码块被称为同步代码块,语法格式:

1
2
3
synchronized(lock){
//操作共享资源代码块
}

lock是一个锁对象,它是同步代码块的关键。当一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,当前线程执行完同步代码块后,所有线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源处理完毕。像是公用电话亭,只有前一个人打完电话出来后,其他的人才可以进入。

更改售票代码,Demo类不变,修改TicketWindow类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TicketWindow implements Runnable{
private int tickets=10;
private Object lock=new Object();//定义任意一个对象,用作同步代码块的锁
public void run(){
while (true){
synchronized (lock){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()+"买票:"+tickets--);
}else break;
}
}
}
}

将有关tickets变量的操作都放到同步代码块中,为了保证线程的持续执行,将代码块放在死循环中,直到tickets<=0时跳出循环。这样就不再出现0和负数的情况了,运行结果往往并不是四个线程都执行了同步代码块,因为线程在获得锁对象时是随机的,运行期间有些线程始终未获得锁对象,所以未能执行同步代码块。

同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”是指锁对象的类型。锁对象的创建不能放到run()方法中,这样每个线程都会创建不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果。

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

同步方法:

除了将共享资源的操作放在synchronized定义的区域内,也可以在方法前使用synchronized关键字来修饰,被修饰的方法为同步方法,他能实现和同步代码块相同的功能,语法格式:

1
synchronized 返回值类型 方法名([参数1,。。。]){。。。}

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。

由于java的每个对象都有一个内置锁,当用synchronized关键字修饰方法时,内置锁会保护整个方法。线程在调用该方法前,需要获得内置锁,否则该线程就会处于阻塞状态。

同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是:同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。

同步方法有时候是静态的,静态方法不需要创建对象就可以直接使用,这时静态同步方法的锁就不会是这个this,而是该方法所在的类的class对象,该对象在装载该类时自动创建,可以用类名.class获取。

使用同步方法改造售票程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TicketWindow implements Runnable{
private int tickets=10;
public void run(){
while (true){
sendTicket();
}
}
public synchronized void sendTicket(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()+"买票:"+tickets--);
}else{
System.exit(0);
}
}
}

注: synchronized关键字也可以修饰静态方法,此时线程如果调用该静态方法,将会锁住整个类。

使用特殊域变量(volatile)实现线程同步:

  • volatile关键字为域变量的访问提供了一种免锁机制
  • 使用volatile修饰域相当于告诉JVM该域可能会被其他线程更新;
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值;
  • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量;

注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域有锁保护的域volatile域可以避免非同步的问题。

使用重入锁(Lock)实现线程同步:

在JDK5之后增加了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized修饰的方法和代码块具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:

1
2
3
ReentrantLock(); //创建一个ReentrantLock实例
lock(); //获得锁
unlock(); //释放锁

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。

之前同步方法实现的售票程序,可以改为用重入锁实现:

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 TicketWindow implements Runnable{
private int tickets=10;
//和同步代码块一样,锁的声明要在执行的代码外
private ReentrantLock lock = new ReentrantLock();
public void run(){
while (true){
sendTicket();
}
}
public void sendTicket(){
lock.lock();//抢到执行权的线程获得锁
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()+"买票:"+tickets--);
lock.unlock();//每次售票后就释放锁
}else{
System.exit(0);
}
}
}

线程通信:

Object类的wait()、notify()和notifyAll()实现通信:

线程执行wait()之后,就放弃了运行资格处于阻塞状态,JVM会把该线程放入等待池中。等待池中的线程不会像锁池中的线程一样自动唤醒,要等待其他线程的notify()或者notifyAll()唤醒该线程并将该线程放入锁池中。

wait()、notify()和notifyAll(),在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这三个方法都是Object类中的方法。

注:等待池中的线程被notify()或者notifyAll()方法唤醒进入到锁池,最后竞争到了锁并且进入了Running状态的话,会从wait现场恢复,执行wait()方法之后的代码。

使用Condition控制线程通信:

JDK5中,提供了很多线程的升级解决方案:

  1. 将同步synchronized替换为显式的Lock操作;
  2. 将Object类中的wait()、notify()和notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;
  3. 一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而JDK5之前一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。

使用阻塞队列控制线程通信:

BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

BlockingQueue提供如下两个支持阻塞的方法:

(1)put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。

(2)take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:

(1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。

(2)在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。

(3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue接口包含如下5个实现类:

  • ArrayBlockingQueue :基于数组实现的BlockingQueue队列。
  • LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
  • PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。
  • SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
  • DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

线程池:

合理使用线程池能够带来三个好处:

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

具体的先不做总结,等遇到应用场景时再做记录。大致产生线程池的方法有:使用Executors工厂类使用Java8增强的ForkJoinPool等等。。


死锁:

两个人A和B进行交易,A说:“我等你的钱到了,我再给你货!”,B说:“我等你的货到了,我再给你钱。”。。。如果不进行干预,结果肯定是无限的等下去。。。

例子中,A和B相当于不同的线程,钱和货相当于锁。两个线程运行时都在等待对方的锁,这样就造成了双方的停滞。这种现象就是死锁。

死锁的四个条件:

  • 互斥条件:资源不能被共享,只能被同一个进程使用。
  • 请求与保持条件:已经得到资源的进程可以申请新的资源。
  • 非剥夺条件:已分配的资源不能从相应的进程中被强制剥夺。
  • 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源。

处理死锁的一般思路:

  • 忽略问题!即鸵鸟算法,当发生了什么问题时,不管他,直接跳过,无视它。
  • 检测死锁并恢复。
  • 资源进行动态分配。
  • 破除上面的四种死锁条件之一。

编写时参考目录:雪飘雪融snow_flower程序员的买房梦