Java基础——有点闹的线程和进程

一、进程和线程

进程

进程就是一个执行中的程序实例,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程。比如在Windows系统中,一个运行的xx.exe就是一个进程。

  • 每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。

线程

线程是指进程中的一个执行任务(控制单元),一个进程中可以运行多个线程。

同一个进程间的多个线程共享该进程的数据

通常情况下:
1、多线程在数据共享上要比多进程更加便捷。
2、一个进程使用多线程,通过提高cpu使用率可以提高效率,因为多线程可以有效的使用系统的资源和提高系统的吞吐量(单位时间内执行的指令数)
线程本身本身并不能提高效率,是曲线救国通过提高资源使用效率来提高系统的效率
3、 Java程序的进程里至少有这么几个线程:主线程, 垃圾回收线程(后台线程)
4、因为CPU在瞬间不断切换去处理各个线程,导致了多线程的执行具有随机性

.
.

一个标准的线程,由线程ID、当前指令指针(PC)、寄存器和堆栈组成。
一个标准的进程,由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
—— [ 编程思想之多线程与多进程(1)——以操作系统的角度述说线程与进程 ]

image.png
image.png

二、并发和并行

  • 单核cpu,并发
  • 多核cpu,并行

单核并发

在单核机器上,“多进程”并不是真正的多个进程在同时执行,而是通过CPU时间分片,操作系统快速在进程间切换而模拟出来的多进程。我们通常把这种情况成为并发。

单核的多个进程,不是“一并发生”的,是cpu高速切换让我们看起来像“一并发生”而已。

多核并行

我们使用的计算机基本上都搭载了多核CPU,这时,我们能真正的实现多个进程并行执行,这种情况叫做并行(一并进行)

多核cpu让多进程的并发有可能变成了并行。

所以我们说得并发,有可能是是并行,也可能是并发,这跟cpu的核心数有关系。
在多核机器上,我们的多个线程可以并行执行在多个核上,进一步提升效率。

例子
举一个并发的小例子,多线程下载文件。
比如我们有一个9m的文件要下载,使用3个线程来下载,那么每个线程下载的分到现在大小为3m。
一方面,因为线程抢占cpu具有随机性,多线程更加容易抢占到被cpu执行机会。
另外一方面,并行/并行工作,会单线程快一些。

三、线程的创建方式

Java的线程有2种创建方式

第一种,继承Thread类

直接定义一个Thread的子类并实例化,从而创建一个新线程。通过start创建线程

class MyThread extends Thread { 
  public void run() { 
    //这里是线程要执行的任务 
  }
}

直接对其调用start方法,即可启动这个线程:

t.start();

第二种方式,实现 Runnable 接口,

实现 Runnable 接口,把 Runnable 作为参数传入 Thread 的构造函数,通过start创建线程

class MyRunnable implements Runnable { 
  ... 
  public void run() { 
    //这里是新线程需要执行的任务 
  }
} 
Runnable r = new MyRunnable();
Thread t = new Thread(r);

调用start方法,即可启动这个线程:

t.start();

.
.
来一个例子吧
线程的启动

public class TestClass {
    
    public static void main(String[] args) {
        System.out.println("main 当前线程:"+Thread.currentThread().getName());
        System.out.println("========");
        
        new MyThread().start();
        new Thread(new RunThread()).start();
    }
}
class MyThread extends Thread{
    public void run(){
        //super.run();
        System.out.println("MyThread run方法执行");
        System.out.println("MyThread run 当前线程 "+Thread.currentThread().getName());
        System.out.println("========");
    }
}



class RunThread implements Runnable{
    @Override
    public void run() {
        System.out.println("RunThread run方法执行");
        System.out.println("RunThread run 当前线程 "+Thread.currentThread().getName());
        System.out.println("========");
    }
}

.
打印

main 当前线程:main
========
MyThread run方法执行
MyThread run 当前线程 Thread-0
========
RunThread run方法执行
RunThread run 当前线程 Thread-1
========

注:+Thread.currentThread().getName()+Thread.currentThread().getName() 可以获得当前线程的名称

.
.
从上面的代码中,虽然看起来很有规律,我们知道线程不是一旦start启动就会被执行,很可能有个等待的过程,被cpu随机切换才正式工作,工作有可能随时被打断,后面我们会再看,现在先来一份简单示例
.
.
线程的被cpu调度具有随机性

public class TestClass {
    public static void main(String[] args) {
        for(int y = 0;y<6;y++){
            new Thread(new RunThread()).start();
            System.out.println("启动 第"+ y +"个线程");
        }
    }
}

class RunThread implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<3;i++){
            System.out.println("run work"+i);
        }
    }
}

.
.
打印

启动 第0个线程
run work0
run work1
run work2
启动 第1个线程
run work0
run work1
启动 第2个线程
run work2
run work0
启动 第3个线程
run work1
run work2
run work0
启动 第4个线程
run work1
run work0
run work2
启动 第5个线程
run work0
run work1
run work1
run work2
run work2

1、打印结果不唯一,随机
2、继承自Thread和实现Runnable都一样,就不另附代码了

.
.
为线程指定名称

主线程默认名称为 main
其他线程默认的名称是 Thread-数字,但是我们可以通过给写一个构造函数,在构造函数里面出入字符串给super的方式给线程指定名称。

我们再来看一份代码。

public class TestClass {
    public static void main(String[] args) {
        new TestThreadA().start();
        new TestThreadB("张三").start();
    }
}

class TestThreadA extends Thread{
    @Override
    public void run() {
        System.out.println("TestThreadA getName "+ getName());
        super.run();
    }
}

class TestThreadB extends Thread{
    TestThreadB(String str){
        super(str);
    }
    @Override
    public void run() {
        System.out.println("TestThreadB getName "+ getName());
        super.run();
    }
}

.
.
输出

TestThreadA getName Thread-0
TestThreadB getName 张三

.
.

两种创建方式对比

**对于第一种方式,继承Thread类 **
A extends Thread:

  • 同份资源不共享
  • 无法继承其他类了

**对于第二种方式,实现 Runnable 接口作为Thread类构造函数 **
class MyRunnable implements Runnable

  • 多个线程共享一个目标资源,适合多线程处理同一份资源。
  • 该类还可以继承其他类

第二种相对比较推荐。

关于run和statr方法

start()

start()方法的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。

run()

run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

三、线程的生命周期/状态

Thread类内部有个public的枚举Thread.State,里边将线程的状态分为:

New(新生)
NEW(新建尚未运行/启动)

Runnable(可运行)
处于可运行状态:正在运行或准备运行
在线程对象上调用start方法后,相应线程便会进入Runnable状态,若被线程调度程序调度,这个线程便会成为当前运行(Running)的线程;

Blocked(被阻塞)
阻塞状态,受阻塞并等待某个监视器锁的线程,处于这种状态。
若一段代码被线程A “上锁” ,此时线程B尝试执行这段代码,线程B就会进入Blocked状态;

Waiting(等待)
通过wait方法进入的等待
当线程等待另一个线程通知线程调度器一个条件时,它本身就会进入Waiting状态;

Time Waiting(计时等待)
通过sleep或wait timeout方法进入的限期等待的状态
计时等待与等待的区别是,线程只等待一定的时间,若超时则不再等待;

Terminated(被终止)
线程终止状态
线程的run方法执行完毕或者由于一个未捕获的异常导致run方法意外终止会进入Terminated状态。

口头的“阻塞”统一指代Blocked、Waiting、Time Waiting其中任一。

来个简图

image.png

四、线程的基本方法/控制线程

要了解的有如下方法

  • start()
  • Thread.sleep()
  • interrupt
  • isAlive()
  • join()
  • yield()
  • wait()
  • **notify() 和 notifyAll() **
  • getPriority() 和 setPriority()

(下文涉及到多线程的代码运行输出部分,结果并不是一定是唯一的,因为cpu调度线程是随机的)

四.1 start()

这是一个实例方法,启动一个线程,但是线程不是已启动就会执行

四.2 isAlive()

这是一个实例方法, 用于判断当前线程是否还“活着”,即线程是否终止,返回true即为“活着”

示例

public class TestClass {
    public static void main(String[] args) {
        
        MyRunnable runTh = new MyRunnable();
        Thread t1 = new Thread(runTh);
        System.out.println("isAlive "+ t1.isAlive());
        t1.start();
        System.out.println("isAlive "+ t1.isAlive());
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("run work");
        
    }
}

.
.
打印

isAlive false
isAlive true
run work

.
.

四.3 interrupt()

interrupt本身是 中断,打断 的意思
这是一个实例方法。每个线程都有一个 中断状态 标识,如果调用 interrupt 会将相应线程的 中断状态 标记为 true,再通过 isInterrupted() 方法即可获得 中断标志 为true。

调用interrupt,能够 打断 那些通过调用 可中断方法 进入 阻塞状态 的线程。
常见的可中断方法有sleep、wait、join,这些方法的内部实现会时不时的检查当前线程的中断状态,若为true会立刻抛出一个InterruptedException异常,从而中断当前线程。

中断捕获异常后,是决定让线程继续运行,还是结束等要根据业务场景才处理。

注:注意,如果线程从来没有调用 可中断方法 ,然后我们就去调用 interrupt 那么线程是不会被中断的。interrupt能够让线程中断的核心原因是sleep、wait、join等可中断方法的内部检查到interrupt状态位true抛异常造成的

使用 interrupt 来停止线程是比较粗暴的(interrupt并不能让线程终止),还有另外一个方法,stop()方法(stop会直接线程终止),这个方法已经被弃用,这个方法更加粗暴。应该怎么合理停止线程我们后面会涉及到。

待会我们会结合sleep方法和interrupt方法写一份小demo

四.4 Thread.sleep()

给当前线程指定睡眠毫秒值

结合sleep和interrupt方法的简单示例

import java.util.Date;
public class TestClass {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        try {
            // 这里我们让主线程睡眠3秒,睡眠期间下面的myThread.interrupt();不会被执行
            // 主线程睡眠期间子线程myThread愉快地每隔一秒打印一次时间
            Thread.sleep(3000);
        }catch (InterruptedException e) {
        }
        // 主线程3秒过后,接着工作,myThread.interrupt()被执行,myThread被终端,不再打印时间
        // 我们这里myThread.interrupt();可以顺利中断myThread是因为myThread之前调用了可中断方法sleep
        myThread.interrupt();
        System.out.println("=== myThread.interrupt() 被执行 ===");
    }
}

class MyThread extends Thread {
    boolean flag = true;
    public void run() {
        // 正常来说,不被影响的下面这段代码会1秒打印一次当前时间
        while (flag) {
            System.out.println("===" + new Date() + "===");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("===捕获 InterruptedException ===");
                return;
            }
        }
    }
}

.
输出

===Sun Apr 30 17:09:28 ICT 2017===
===Sun Apr 30 17:09:29 ICT 2017===
===Sun Apr 30 17:09:30 ICT 2017===
=== myThread.interrupt() 被执行 ===
===捕获 InterruptedException ===

可见,myThread.interrupt()成功地中断了myThread线程。
.
.

四.5 join()

这是一个实例方法,在A线程中对线程B调用join方法会导致A线程暂时处于waiting,等线程B运行完毕后再接着运行线程A。
也就是说,把当前线程还没执行的部分“接到”另一个线程后面去,另一个线程运行完毕后,当前线程再接着运行。join方法有以下重载版本:

public final synchronized void join() throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException;
public final synchronized void join(long millis, int nanos) throws InterruptedException;

无参数的join表示当前线程一直等到另一个线程运行完毕,这种情况下当前线程会处于Wating状态;

带参数的表示当前线程只等待指定的时间,这种情况下当前线程会处于Time Waiting状态。当前线程通过调用join方法进入Time Waiting或Waiting状态后,会释放已经获取的锁。实际上,join方法内部调用了Object类的实例方法wait

join改变线程执行结果

示例代码

import java.util.Date;
public class TestClass {
    public static void main(String[] args) {
        
        System.out.println("主线程工作中 "+Thread.currentThread().getName());
        MyThread myThread = new MyThread();
        myThread.start();
        
        System.out.println("主线程 的状态(子线程join之前) "+Thread.currentThread().getState());
        // 子线程 join,主线程停止等待
        try {
            myThread.join();
            System.out.println("主线程 的状态(子线程join之后) "+Thread.currentThread().getState());
        }catch (InterruptedException e) {
            System.out.println("===主线程 捕获 InterruptedException ===");
        }
        for(int i=0;i<8;i++){
            System.out.println("主线程for循环 "+Thread.currentThread().getName() +" == "+i);
        }
        
    }
}

class MyThread extends Thread {
    
    public void run() {
        for(int i=0;i<5;i++){
            System.out.println("===" + new Date() + "===");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("===捕获 InterruptedException ===");
                return;
            }
        }
    }
}

输出

主线程工作中 main
主线程 的状态(子线程join之前) RUNNABLE
===Sun Apr 30 20:00:07 ICT 2017===
===Sun Apr 30 20:00:08 ICT 2017===
===Sun Apr 30 20:00:09 ICT 2017===
===Sun Apr 30 20:00:10 ICT 2017===
===Sun Apr 30 20:00:11 ICT 2017===
主线程 的状态(子线程join之后) RUNNABLE
主线程for循环 main == 0
主线程for循环 main == 1
主线程for循环 main == 2
主线程for循环 main == 3
主线程for循环 main == 4
主线程for循环 main == 5
主线程for循环 main == 6
主线程for循环 main == 7

可以看到,在myThread.start();执行之后,主线程停止执行,直到子线程的run工作后主线程再继续工作。

如果我们上面代码中的
myThread.join();
这行代码和其try备注掉,那么打印结果很可能就是

主线程工作中 main
主线程 的状态(子线程join之前) RUNNABLE
主线程for循环 main == 0
主线程for循环 main == 1
主线程for循环 main == 2
主线程for循环 main == 3
主线程for循环 main == 4
主线程for循环 main == 5
主线程for循环 main == 6
主线程for循环 main == 7
===Sun Apr 30 20:04:03 ICT 2017===
===Sun Apr 30 20:04:05 ICT 2017===
===Sun Apr 30 20:04:06 ICT 2017===
===Sun Apr 30 20:04:07 ICT 2017===
===Sun Apr 30 20:04:08 ICT 2017===

通过这两段输出,我们可以看到join的作用了。
还有,子线程调用join时,主线程还是Runnable状态

四.6 getPriority() 和 setPriority()

getPriority() 获得线程的优先级数值
setPriority() 设置线程的优先级数值

优先级范围
线程存在优先级,优先级范围在1~10之间。

默认优先级和优先级常量
线程默认优先级是5,Thread类中有三个常量,定义线程优先级范围:
static int MAX_PRIORITY
线程可以具有的最高优先级。
static int MIN_PRIORITY
线程可以具有的最低优先级。
static int NORM_PRIORITY
分配给线程的默认优先级。

JVM线程调度程序是基于优先级的调度机制。在大多数情况下,
1、当前运行的线程优先级将大于或等于线程池中任何线程的优先级。
2、优先级高的线程被cpu调度的概率大于优先级低的线程

1、不是说优先级低在在优先级高的面前就不执行了,只是优先级高的执行频率比较高,而优先级低的执行一小会就会被赶出来。
2、当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种操作。

设置优先级

示例代码

public class TestPriority {

   public static void main(String[] args) {
      Thread t1 = new Thread(new T1());
      Thread t2 = new Thread(new T2());
      t1.setPriority(Thread.NORM_PRIORITY+3);  //设置优先级的值
      t1.start();
      t2.start();
    }
 }

 class T1 implements Runnable {
   public void run() {
    for(int i = 0 ;i <50 ; i++)
     System.out.println("T1:"+i);
    }
  }

  class T2  implements Runnable {
   public void run() {
    for(int i = 0 ;i <50 ; i++)
     System.out.println("------T2::"+i);
    }
  }

四.7 yield()

这是一个静态方法,作用是让当前线程“让步”,目的是让其他线程有更大的可能被系统调度,这个方法不会释放锁。

yield() 的“让步”只是让一小会,一小会之后就接着工作了。
yield操作时,线程还是Runnable状态。

调用yield()做出让步

代码

public class TestClass {
    public static void main(String[] args) {
        MyThreadA myThreadA = new  MyThreadA();
        MyThreadB myThreadB = new MyThreadB();
        myThreadA.start();
        myThreadB.start();
    }
}

class MyThreadA extends Thread {
    public void run() {
        for(int i=0;i<12;i++){
            System.out.println("MyThreadA run work  "+i);
        }
    }
}

class MyThreadB extends Thread {
    public void run() {
        for(int i=0;i<12;i++){
            if(i/3 == 0){
                // 当循环到模以3为0的时候,就做出一小会的“让步”
                yield();
            }
            System.out.println("MyThreadB ===== run work  "+i);
        }
    }
}

.
输出
(某一次的输出)

MyThreadA run work  0
MyThreadB ===== run work  0
MyThreadA run work  1
MyThreadB ===== run work  1
MyThreadA run work  2
MyThreadA run work  3
MyThreadA run work  4
MyThreadA run work  5
MyThreadA run work  6
MyThreadA run work  7
MyThreadA run work  8
MyThreadB ===== run work  2
MyThreadB ===== run work  3
MyThreadB ===== run work  4
MyThreadA run work  9
MyThreadB ===== run work  5
MyThreadA run work  10
MyThreadB ===== run work  6
MyThreadA run work  11
MyThreadB ===== run work  7
MyThreadB ===== run work  8
MyThreadB ===== run work  9
MyThreadB ===== run work  10
MyThreadB ===== run work  11

四.8 wait()

wait方法是Object类中定义的实例方法。在指定对象上调用wait方法能够让当前线程进入阻塞状态(前提时当前线程持有该对象的内部锁(monitor)),此时当前线程会释放已经获取的那个对象的内部锁,这样一来其他线程就可以获取这个对象的内部锁了。当其他线程获取了这个对象的内部锁,进行了一些操作后可以调用notify方法来唤醒正在等待该对象的线程。

关于wait()涉及到线程锁和线程通信问题,后文的关于会有相关参考代码。

.
.

四.9 **notify() 和 notifyAll() **

notify/notifyAll方法也是Object类中定义的实例方法。作用是唤醒正在等待相应对象的线程

notify() 唤醒 wait pool 一个等待该对象的线程
notifyAll() 唤醒 wait pool 所有等待该对象的线程

关于notify()/notifyAll()涉及到线程锁和线程通信问题,后文的关于会有相关参考代码。
.
.

四.10 如何停止线程

注意:
1、interrupt 并无法真正停止线程
2、Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!

正常情况下线程什么时候回停止?
run方法执行完毕,该线程就会正常结束。
(但有时候线程是永远无法结束的,比如while(true)。)

1、利用run里面的标志位结束线程

run里面while,用boolean标志位控制

代码

public class TestClass {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        // 延缓一下,不至于线程马上关闭,为了方便打印演示结果
        for(int i=0;i<30;i++){
            if(i%10==0)
                System.out.println("in thread main i=" + i);
        }
        myThread.shutDown();
        System.out.println("myThread.shutDown() 执行");
    }
}

class MyThread extends Thread {
    // 停止线程的标志位  (run结束,线程就结束)
    private boolean flag = true;
    public void run() {
        int i = 0;
        while (flag == true) {
            System.out.println("MyThread  run  " + i++);
        }
    }

    public void shutDown() {
        // 自己定义这个方法,调用时让线程停止
        flag = false;
    }
}

输出

某次输出

in thread main i=0
in thread main i=10
MyThread  run  0
MyThread  run  1
in thread main i=20
myThread.shutDown() 执行
MyThread  run  2

.
.
再来一次输出

in thread main i=0
in thread main i=10
in thread main i=20
myThread.shutDown() 执行
MyThread  run  0

可见,停止进程后不是说run里面的代码就绝对马上停止,可能还会执行个一次两次,但是关闭确实是实现的了。

(有时间再补上其他的方式)
待添加

下面这副图描述了线程从创建到消亡之间的状态:

image.png

.
.
.

五、线程同步/多线程安全

一般情况下,多线程之间各做各的,没什么冲突和影响。

多线程安全问题的产生
当我们多个线程访问同一个共享数据,很可能由于一个线程操作了共享数据,还没有语句还没执行完,另一个线程被cpu调度又被执行也操作了共享数据。导致共享数据的错误。

多线程安全问题的原因
1、多个线程访问出现延迟。
2、线程随机性。
3、操作了共享数据,这个是核心

多线程安全问题的解决
利用线程锁来解决。
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。

五.1、多线程问题的产生

我们通过一个经典的卖票代码来演示多线程安全问题的产生
.
.

public class TestClass {
    public static void main(String[] args) {
        SellRunnable sell = new SellRunnable();
        new Thread(sell, "1号窗口").start();
        new Thread(sell, "2号窗口").start();
        new Thread(sell, "3号窗口").start();
    }
}

class SellRunnable implements Runnable {
    private int num = 10;

    @Override
    public void run() {
        while (true) {
            if (num > 0) {
                try {
                    // 通过 Thread.sleep(10) 的延迟可以更好地模拟演示多线程安全问题
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出第" + num-- + "张票!");
            }
        }
    }
}

.
.
打印结果

1号窗口卖出第8张票!
2号窗口卖出第10张票!
3号窗口卖出第9张票!
1号窗口卖出第7张票!
2号窗口卖出第6张票!
3号窗口卖出第6张票!
1号窗口卖出第5张票!
3号窗口卖出第4张票!
2号窗口卖出第4张票!
2号窗口卖出第3张票!
3号窗口卖出第2张票!
1号窗口卖出第2张票!
1号窗口卖出第0张票!
3号窗口卖出第1张票!
2号窗口卖出第-1张票!

(打印结果有可能出现多线程问题有可能不会,多试几次总会看到问题)

如上结果,最直接的我们看到卖出了 -1 张票,这肯定不合逻辑
还有就是有的票重复卖出,比如第6张,第4张。
还有其他明显的问题。

就这样,多线程安全问题产生了。
至于原因,我们已经说过了,就是甲线程被调度,操作还没结束,乙线程又被调度,两者都操作同一个数据。

五.2、 多线程安全问题的解决

五.2.1、同步代码块

格式

synchronized(obj)
{
    //obj表示同步监视器,是同一个同步对象
    /**.....
        TODO SOMETHING
    */
}

解决示例

public class TestClass {
    public static void main(String[] args) {
        SellRunnable sell = new SellRunnable();
        new Thread(sell, "1号窗口").start();
        new Thread(sell, "2号窗口").start();
        new Thread(sell, "3号窗口").start();
    }
}

class SellRunnable implements Runnable {
    private int num = 10;
    String str = "";// 这句代码的位置很重要,如果放在run里面,那么synchronized (str)的obj就是不同的对象,会产生不同的监视器

    @Override
    public void run() {
        while (true) {
            synchronized (str) {
                if (num > 0) {
                    try {
                        // 通过 Thread.sleep(1000)  在同步代码块更好的模拟不同窗口买票的情况 
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出第" + num-- + "张票!");
                }
            }
        }
    }
}

.
.
输出结果

1号窗口卖出第10张票!
1号窗口卖出第9张票!
1号窗口卖出第8张票!
1号窗口卖出第7张票!
3号窗口卖出第6张票!
3号窗口卖出第5张票!
1号窗口卖出第4张票!
1号窗口卖出第3张票!
2号窗口卖出第2张票!
1号窗口卖出第1张票!

如上,利用了同步代码块解决了问题,这是线程安全的。

注意点:
1、这种打印情况不好复现,有很大可能出现所有票都是 1号窗口 卖出的情况(如果sleep的时间是10毫秒就更难复现了,所以我们sleep调为1000),如果想更好地复现,我们可以总票数调为100张,多线程卖票就可以很好打印出结果。
2、synchronized (obj)传入的实参必须是一个唯一的对象,这样不同的线程才是同一个面向同于个监视器,才能同步,如果监视器不一样,就谈不上同步了。

.
.

你必须知道的synchronized(obj)

简单理解版

任意类型的对象都有一个标志位,该标志位具有0、1 两种状态,其开始状态为1。

当执行synchronized(object)语句后,object对象的标志位变为0状态,直到执行完整个synchronized语句中的代码块后又回到1状态。

一个线程执行到synchronized(object)语句处时,先检查object对象的标志位,如果为0状态,表明已经有另外的线程的执行状态正在有关的同步代码块中,这个线程将暂时阻塞,让出CPU资源,直到另外的线程执行完有关的同步代码块,将object 对象的标志位恢复到1状态,这个阻塞就被取消,线程能够继续往下执行,并将object 对象的标志位变为0状态,防止其他线程再进入有关的同步代码块中。

如果有多个线程因等待同一对象的标志位而处于阻塞状态时,当对象的标志位恢复到1状态时,只会有一个线程能够继续运行,其他线程仍然处于阻塞等待状态。

我们反复提到有关的同步代码块,是指不仅同一个代码块在多个线程间可以实现同步(像上面例子一样),若干个不同的代码块也可以实现相互之间的同步,只要各synchronized(object)语句中的object完全是同一个对象就可以。

画张图吧

image.png

原理版

当线程执行到synchronized的时候检查传入的实参对象,并尝试得到该对象的锁旗标(就是我们上面讲的标志位)。

如果得不到,那么此线程就会被加入到一个与该对象的锁旗标相关连的等待线程池中,一直等到该对象的锁旗标被归还,池中的等待线程就可能会得到该旗标,然后继续执行下去。

当线程执行完成同步代码块时,就会自动释放它占有的同步对象的锁旗标。一个用于synchronized语句中的对象称为一个监视器,当一个线程获得了synchronized(object)语句中的代码块的执行权,即意味着它锁定了监视器在一段时间内,只能有一个线程可以锁定监视器

所有其他的线程在试图进入已锁定的监视器时将被挂起,直到锁定了监视器的线程执行完synchronized(object)语句中的代码块,即监视器被解锁为止,另外的线程才可以进入并锁定监视器。

一个刚锁定了监视器的线程在监视器被解锁后可以再次进入并锁定同一监视器,好比篮球运动员的篮球出手后可以再次去抢回来一样。另外当在同步块中遇到break语句或扔出异常时,线程也会释放该锁旗标

其实,程序并不能控制CPU的切换,程序是不可能抱着CPU的大腿不让他走的。当CPU进入了一段同步代码块中执行,CPU是可以切换到其他线程的,只是在准备执行其他线程的代码时,发现其他线程处于阻塞状态,CPU又会回到先前的线程上。大家也看到同步处理后,程序的运行速度比原来没有使用同步处理前更慢了,因为系统要不停地对同步监视器进行检查,需要更多的开销。同步是以牺牲程序的性能为代价的,如果我们能够确定程序没有安全性的问题,就没必要使用同步控制

小结

  • 1、synchronized (obj)传入的实参必须是同一个对象,可以是一个字符串对象,可以传入this用当前类来表示这个对象等,但是不可以在run里面new出对象,也就是要确保对象的唯一性。

  • 2、synchronized (obj)传入的实参对象就是一个锁旗帜,(锁旗帜为了方便理解也可以认为是一个标志位)

  • 3、一个线程对象尝试获取锁旗帜,如果能获取那么就顺利执行逻辑,如果获取不到就会被放进 等待线程池 ,被挂起,处于阻塞状态

  • 4、同步代码块中,一个锁旗帜只能由于一个线程对象所持有。

  • 5、一个刚锁定了监视器的线程在监视器被解锁后可以再次进入并锁定同一监视器,不是说解锁之后就一定是其他线程获得锁旗帜。

  • 6、当CPU进入了一段同步代码块中执行,CPU是可以切换到其他线程的,只是在准备执行其他线程的代码时,发现其他线程处于阻塞状态,CPU又会回到先前的线程上当CPU进入了一段同步代码块中执行,CPU是可以切换到其他线程的,只是在准备执行其他线程的代码时,发现其他线程处于阻塞状态,CPU又会回到先前的线程上

  • 7、多线程同步会更多地消耗cpu的性能,如果可以确定没有线程安全问题,多线程没必要进行同步。

.
.
.

五.2.2、 同步函数/同步方法

格式

在方法上加上synchronized修饰符即可。(一般写在run方法之外!)

synchronized 返回值类型 方法名(参数列表)
{
    /**.....
        TODO SOMETHING
    */
}

同步方法的同步监听器其实的是 this
(因为static不能调用this,因此如果是静态同步方法,默认监听器是当前方法所在类的.class对象)

同步方法 示例

public class TestClass {
    public static void main(String[] args) {
        SellRunnable sell = new SellRunnable();
        new Thread(sell, "1号窗口").start();
        new Thread(sell, "2号窗口").start();
        new Thread(sell, "3号窗口").start();
    }
}

class SellRunnable implements Runnable {
    private int num = 10;
    String str = "";

    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }
    
    public synchronized void sellTicket(){
        
        if (num > 0) {
            try {
                // 通过 Thread.sleep(1000)  在同步代码块更好的模拟不同窗口买票的情况 
                Thread.sleep(1000);
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖出第" + num-- + "张票!");
        }
    }
}

可见,在函数定义前使用synchronized 关键字也能够很好实现线程间的同步。

当有一个线程进入了synchronized 修饰的方法(获得监视器),其他线程就不能进入同一个对象的所有使用了synchronized 修饰的方法, 直到第一个线程执行完它所进入的synchronized 修饰的方法为止(离开监视器)。
.
.

实现代码块与函数之间的可以实现同步

实现代码块与函数之间的可以实现同步,前提是两者使用的必须是同一个监视器。

五.2.3、 同步锁 ReentrantLock

Lock是java.util.concurrent.locks包下的接口,ReentrantLock类是唯一一个Lock接口的实现类,它的意思是可重入锁,我们这里先演示简单实用,后面会有专门的章节谈论Lock。

Java类库中为我们提供了能够给临界区“上锁”的ReentrantLock类,它实现了Lock接口。

关于“临界区”,后文我们会涉及。

示例代码

import java.util.concurrent.locks.ReentrantLock;
public class TestClass {
    public static void main(String[] args) {
        SellRunnable sell = new SellRunnable();
        new Thread(sell, "1号窗口").start();
        new Thread(sell, "2号窗口").start();
        new Thread(sell, "3号窗口").start();
    }
}

class SellRunnable implements Runnable {
    private int num = 10;
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            sell();
        }
    }
    
    public void sell(){
        lock.lock(); // 上锁
        try{
            if (num > 0) {
                try {
                    // 通过 Thread.sleep(1000)  在同步代码块更好的模拟不同窗口买票的情况 
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出第" + num-- + "张票!");
            }
        }finally {
            lock.unlock(); // 解锁
        }
    }
}

用法和同步方法类似,注意上锁和手动解锁。

.
.
输出依然没有安全问题的产生。

六、竞争条件与临界区

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services 等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

多线程同时执行下面的代码可能会出错:

public class Counter {
    protected long count = 0;
    public void add(long value){
        this.count = this.count + value;   
    }
}

想象下线程 A 和 B 同时执行同一个 Counter 对象的 add()方法,我们无法知道操作系统何时会在两个线程之间切换。JVM 并不是将这段代码视为单条指令来执行的,而是按照下面的顺序:

从内存获取 this.count 的值放到寄存器
将寄存器中的值增加 value
将寄存器中的值写回内存

观察线程 A 和 B 交错执行会发生什么:

this.count = 0;
A: 读取 this.count 到一个寄存器 (0)
B: 读取 this.count 到一个寄存器 (0)
B: 将寄存器的值加 2
B: 回写寄存器值(2)到内存. this.count 现在等于 2
A: 将寄存器的值加 3
A: 回写寄存器值(3)到内存. this.count 现在等于 3

两个线程分别加了 2 和 3 到 count 变量上,两个线程执行结束后 count 变量的值应该等于 5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是 0。然后各自加了 2 和 3,并分别写回内存。最终的值并不是期望的 5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程 A,但实际中也可能是线程 B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。

竞争条件(race condition)
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。

临界区(critical area)
导致竞态条件发生的代码区称作临界区。

上例中 add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。

七、Lock与synchronized

七.1、Lock与synchronized

Java中可以使用 Lock 和 synchronized 都可以实现对某个共享资源的同步,同时也可以实现对某些过程的原子性操作。Lock内部也使用了synchronized。

通常用法

  • synchronized:
    在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

  • Lock:
    需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

性能上的一点事
在 JDK1.5 中,synchronized 是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java 提供的 Lock 对象,性能更高一些。Brian Goetz 对这两种锁在 JDK1.5、单核处理器及双 Xeon 处理器环境下做了一组吞吐量对比的实验,发现多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock 则能基本保持在同一个比较稳定的水平上。但与其说 ReetrantLock 性能好,倒不如说 synchronized 还有非常大的优化余地。
于是到了 JDK1.6,发生了变化,对 synchronize 加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在 JDK1.6 上 synchronize 的性能并不比 Lock 差。官方也表示,他们也更支持 synchronize,在未来的版本中还有优化余地,所以还是提倡在 synchronized 能实现需求的情况下,优先考虑使用 synchronized 来进行同步。


Lock可以使用Condition进行线程之间的调度,Synchronized则使用Object对象本身的notify, wait, notityAll调度机制,这两种调度机制有什么异同呢?

Condition是Java5以后出现的机制,它有更好的灵活性,而且在一个对象里面可以有多个Condition(即对象监视器),线程可以注册在不同的Condition,从而可以有选择性的调度线程,更加灵活。

Synchronized就相当于整个对象只有一个单一的Condition(即该对象本身)所有的线程都注册在它身上,线程调度的时候之后调度所有得注册线程,没有选择权,会出现相当大的问题 。

七.2、Lock

七.2.1 Lock接口和方法

Java 5 中引入了新的锁机制——java.util.concurrent.locks 中的显式的互斥锁:Lock 接口,它提供了比synchronized 更加广泛的锁定操作。

先来看一下Lock接口

public interface Lock { 
  void lock(); 
  void lockInterruptibly() throws InterruptedException; 
  boolean tryLock(); 
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
  void unlock(); 
  Condition newCondition();
}

方法介绍

  • lock方法:用来获取锁,在锁被占用时它会一直阻塞,并且这个方法不能被中断;
  • lockInterruptibly方法:在获取不到锁时也会阻塞,它与lock方法的区别在于阻塞在该方法时可以被中断;
  • tryLock方法:也是用来获取锁的,它的无参版本在获取不到锁时会立刻返回false,它的计时等待版本会在等待指定时间还获取不到锁时返回false,计时等待的tryLock在阻塞期间也能够被中断。使用tryLock方法的典型代码如下:
if (myLock.tryLock()) { 
  try { 
    … 
  } finally { 
    myLock.unlock(); 
  }
} else { 
//做其他的工作
}
  • unlock方法:用来释放锁;

  • newCondition方法:用来获取当前锁对象相关的条件对象。


七.2.2 关于锁的一些概念

可重入锁

可重入锁的概念是自己可以再次获取自己的内部锁。举个例子,比如一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的(如果不可重入的锁的话,此刻会造成死锁)。说的更高深一点可重入锁是一种递归无阻塞的同步机制。

读写锁

读写锁拆成读锁和写锁来理解。读锁可以共享,多个线程可以同时拥有读锁,但是写锁却只能只有一个线程拥有,而且获取写锁的时候其他线程都已经释放了读锁,而且该线程获取写锁之后,其他线程不能再获取读锁。
简单的说就是写锁是排他锁,读锁是共享锁。

公平锁和非公平锁

获取锁涉及到的两个概念即 公平和非公平

  • 公平锁
    公平表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO顺序。

  • 非公平锁
    非公平就是一种获取锁的抢占机制,和公平相对就是先来不一定先得,这个方式可能造成某些线程饥饿(一直拿不到锁)。

关于锁头的其他详细分类可以查看 Java锁的种类以及辨析

Lock 接口有 3 个实现它的类

  • ReentrantLock 重入锁
  • ReetrantReadWriteLock.ReadLock 读锁
  • ReetrantReadWriteLock.WriteLock 写锁

Lock 必须被显式地创建、锁定和释放,为了可以使用更多的功能,一般用 ReentrantLock 为其实例化。为了保证锁最终一定会被释放(可能会有异常发生),要把互斥区放在 try 语句块内,并在 finally 语句块中释放锁,尤其当有 return 语句时,return 语句必须放在 try 字句中,以确保 unlock()不会过早发生,从而将数据暴露给第二个任务。因此,采用 lock 加锁和释放锁的一般形式如下:

Lock lock = new ReentrantLock();//默认使用非公平锁,如果要使用公平锁,需要传入参数true 
........  
lock.lock();  
try {  
     //更新对象的状态  
    //捕获异常,必要时恢复到原来的不变约束  
   //如果有return语句,放在这里  
 finally {  
       lock.unlock();        //锁必须在finally块中释放
}

关于线程的同步的先到这里。

八、线程间的通信

线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。

我们用一个经典的生产者和消费者的代码来演示这个线程间的通信。

需求:生产者产出一个产品,消费者就消费一个产品
需要控制好通信和线程安全的问题,不能出现
1、产品还没生产就被消费了
2、一个产品被多次重复消费
3、不能生产了一批就开始消费(要求生产一个就马上消费一个)

涉及到进程间通讯的几个方法

写代码之前,我们先来看一下几个方法

  • wait()
    让当前线程放弃监视器进入等待,直到其他线程调用同一个监视器并调用notify()或notifyAll()为止。

  • notify()
    唤醒在同一对象监听器中调用wait方法的第一个线程。

  • notifyAll()
    唤醒在同一对象监听器中调用wait方法的所有线程。

wait()、notify()、notifyAll()的调用细节

wait()、notify()、notifyAll(),这三个方法属于Object 不属于 Thread

这三个方法必须由同步监视对象来调用,两种情况:

  • 1.synchronized修饰的方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中调用这三个方法;
  • 2.synchronized修饰的同步代码块,同步监视器是括号里的对象,所以必须使用该对象调用这三个方法;

可要是我们使用的是Lock对象来保证同步的,系统中不存在隐式的同步监视器对象,那么就不能使用者三个方法了,那该咋办呢?

此时,Lock代替了同步方法或同步代码块,Condition代替了同步监视器的功能;
Condition对象通过Lock对象的newCondition()方法创建;

里面方法包括:

  • await(): 等价于同步监听器的wait()方法;
  • signal(): 等价于同步监听器的notify()方法;
  • signalAll(): 等价于同步监听器的notifyAll()方法;

线程间通信的示例

public class TestClass {
    public static void main(String[] args) {
        Goods g = new Goods();
        new Thread(new Producer(g)).start();
        new Thread(new Consumer(g)).start();
    }
}
class Goods{
    private String name;
    private String price;
    private Boolean isimpty = Boolean.TRUE;//内存区为空!
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPrice() {
        return price;
    }
    public void setPrice(String sex) {
        this.price = sex;
    }
    
    public void setGoods(String name,String sex){
        synchronized (this) {
            // 生产的产品不为空,还没被消费,就不生产,放弃监视器进入等待
            while(isimpty.equals(Boolean.FALSE)){ 
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.name = name;//为空的话生产者创造!
            this.price = sex;
            isimpty = Boolean.FALSE;//创造结束后修改属性!
            System.out.println("生产 +++ 产品:"+getName()+ ",  "+"价格:"+getPrice());
            this.notifyAll();
        }
    }
    
    public void getGoods(){
        synchronized (this) {
            // 生产的产品为空,没有产品可以被消费,就不消费,放弃监视器进入等待
            while(isimpty.equals(Boolean.TRUE)){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("消费 --- 产品:"+getName()+ ",  "+"价格:"+getPrice());
            isimpty = Boolean.TRUE;
            this.notifyAll();
        }
    }
}

class Producer implements Runnable{
    private Goods pgs;
    public Producer(Goods p) {
        super();
        this.pgs = p;
    }

    @Override
    public void run() {
        for (int i = 0; i < 12; i++) {
            pgs.setGoods("产品编号为:"+i, i+10+".00");
            
        }
    }
}

class Consumer implements Runnable{
    private Goods cgs;
    public Consumer(Goods p) {
        super();
        this.cgs = p;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            cgs.getGoods();
        }
    }
}

如上代码,

  • 当我们生产时候,
    如果发现已经有产品存在没被消费,那么这个生产的线程就wait,不接着往下执行生产的代码了;
    如果产品为空,就这顺利执行生产的代码,并且调用 this.notifyAll(); 通知消费者线程可以来消费了

  • 当我们消费的时候
    如果没有产品可以被消费,那么消费者线程就wait,不接着往下执行消费的代码了;
    如果产品不为空,就顺利执行消费的代码,消费完成之后调用 this.notifyAll();唤醒之前因为生产完被职位wait的生产者线程。

.
.
输出

生产 +++ 产品:产品编号为:0,  价格:10.00
消费 --- 产品:产品编号为:0,  价格:10.00
生产 +++ 产品:产品编号为:1,  价格:11.00
消费 --- 产品:产品编号为:1,  价格:11.00
生产 +++ 产品:产品编号为:2,  价格:12.00
消费 --- 产品:产品编号为:2,  价格:12.00
生产 +++ 产品:产品编号为:3,  价格:13.00
消费 --- 产品:产品编号为:3,  价格:13.00
生产 +++ 产品:产品编号为:4,  价格:14.00
消费 --- 产品:产品编号为:4,  价格:14.00
生产 +++ 产品:产品编号为:5,  价格:15.00
消费 --- 产品:产品编号为:5,  价格:15.00
生产 +++ 产品:产品编号为:6,  价格:16.00
消费 --- 产品:产品编号为:6,  价格:16.00
生产 +++ 产品:产品编号为:7,  价格:17.00
消费 --- 产品:产品编号为:7,  价格:17.00
生产 +++ 产品:产品编号为:8,  价格:18.00
消费 --- 产品:产品编号为:8,  价格:18.00
生产 +++ 产品:产品编号为:9,  价格:19.00
消费 --- 产品:产品编号为:9,  价格:19.00
生产 +++ 产品:产品编号为:10,  价格:20.00
消费 --- 产品:产品编号为:10,  价格:20.00
生产 +++ 产品:产品编号为:11,  价格:21.00
消费 --- 产品:产品编号为:11,  价格:21.00

利用 代码展示线程状态

参考:

Java核心技术点之多线程
Java并发编程:Thread类的使用
java 线程的优先级Priority

推荐阅读更多精彩内容

  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 2,830评论 12 42
  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    宋依蓝阅读 5,237评论 3 88
  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 495评论 0 1
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 1,618评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,181评论 1 18