一、程序、进程、线程的区别与联系
程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行,这种执行的程序称之为进程,也就是说进程是系统进行资源分配和调度的一个独立单位,每个进程都有自己单独的地址空间。所以说程序与进程的区别在于,程序是指令的集合,是进程运行的静态描述文本,而进程则是程序在系统上顺序执行时的动态活动。
但是进程存在着很多缺陷,主要集中在两点:
- 进程只能在同一时间干一件事情,如果想同时干两件事或多件事情,进程就无能为力了。
- 进程在执行的过程中如果由于某种原因阻塞了,例如等待输入,整个进程就会挂起,其他与输入无关的工作也必须等待输入结束后才能顺序执行。
为了解决上述两点缺陷,引入了线程这个概念。
线程是进程的一个实体,也是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,有时又被称为轻权进程或轻量级进程,相对进程而言,线程是一个更加接近于执行体的概念,进程在执行过程中拥有独立的内存单元,而线程自己基本上不拥有系统资源,也没有自己的地址空间,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。除了CPU 之外,计算机内的软硬件资源的分配与线程无关,但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
进程和线程的主要差别在于操作系统并没有将多个线程看作多个独立的应用,来实现进程的调度和管理以及资源分配。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些,对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程,每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
进程和线程做了很好的类比:
计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务
进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
一个车间里,可以有很多工人。他们协同完成一个任务。
线程就好比车间里的工人。一个进程可以包括多个线程。
车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫”互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做”信号量”(Semaphore),用来保证多个线程不会互相冲突。
不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
二、创建线程的三种方式
2.1.通过继承 Thread 来创建线程
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
- setName:设置线程的名字
- getName:设置线程的名字
public class NewThread extends Thread{
/*public NewThread(String name) {
super(name);//调用父类的有参构造器
}*/
public void run(){
//输出1-10
for (int i = 0; i < 10; i++) {
System.out.println(this.getName()+i);
}
}
}
测试类如下:
public class Test {
public static void main(String[] args) throws InterruptedException {
//Thread.currentThread()获取当前正在执行的线程
Thread.currentThread().setName("主线程");
//主线程也输出十个数字
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"1----------"+i);
}
System.out.println("------------------制造其他的线程,跟主线程抢夺资源----------------");
//创建线程
NewThread newThread = new NewThread();
//设置线程名称
newThread.setName("子线程");
// 不能直接调用run方法,直接调用就会被当做一个普通方法,想要 tt子线程 真正其作用要使用start方法启动线程
newThread.start();//启动线程
//主线程也输出十个数字
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"2----------"+i);
}
}
}
通过构造器设置线程的名字,如下:
public class NewThread extends Thread{
public NewThread(String name) {
super(name);//调用父类的有参构造器
}
public void run(){
//输出1-10
for (int i = 0; i < 10; i++) {
System.out.println(this.getName()+i);
}
}
}
后面再创建线程的时候,即可设置线程名,不需要通过setName方法设置
public class Test {
public static void main(String[] args) throws InterruptedException {
//Thread.currentThread()获取当前正在执行的线程
Thread.currentThread().setName("主线程");
//主线程也输出十个数字
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"1----------"+i);
}
System.out.println("------------------制造其他的线程,跟主线程抢夺资源----------------");
//创建线程 同时设置名称
NewThread newThread = new NewThread("子线程");
// 不能直接调用run方法,直接调用就会被当做一个普通方法,想要 tt子线程 真正其作用要使用start方法启动线程
newThread.start();//启动线程
//主线程也输出十个数字
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"2----------"+i);
}
}
}
案例买火车票:
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name) {
super(name);
}
//定义车票数量
static int ticketNum = 10;//多个对象共享10张机票
//每个窗口都是一个线程对象,每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票
for (int i = 0; i < 100; i++) {
if(ticketNum> 0){
//判断票数
System.out.println("我在:"+this.getName()+"买到了从北京到丽江的第:"+ticketNum-- +"张车票");
}
}
}
}
测试代码
public class Test02 {
public static void main(String[] args) {
BuyTicketThread t1 = new BuyTicketThread("窗口1");
t1.start();
BuyTicketThread t2 = new BuyTicketThread("窗口2");
t2.start();
BuyTicketThread t3 = new BuyTicketThread("窗口3");
t3.start();
}
}
2.2.通过实现 Runnable 接口来创建线程
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:
public void run()
可以重写该方法,重要的是理解的 run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。Thread定义了几个构造方法,下面的这个是我们经常使用的:
Thread(Runnable threadOb,String threadName);
这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。新线程创建之后,你调用它的start()方法它才会运行。
void start();
案例如下:
public class BuyTicketThread2 implements Runnable{
//定义车票数量
static int ticketNum = 10;//多个对象共享10张机票
@Override
public void run() {
//每个窗口后面有100个人在抢票
for (int i = 0; i < 100; i++) {
if(ticketNum> 0){
//判断票数
//Thread.currentThread().getName() 获取线程名字
System.out.println("我在:"+Thread.currentThread().getName()+"买到了从北京到丽江的第:"+ticketNum-- +"张车票");
}
}
}
}
测试代码如下:
public class Test03 {
public static void main(String[] args) {
//创建一个线程对象
BuyTicketThread2 t = new BuyTicketThread2();
Thread t1 = new Thread(t, "窗口1");
t1.start();
Thread t2 = new Thread(t, "窗口2");
t2.start();
Thread t3= new Thread(t, "窗口3");
t3.start();
}
}
实际开发中,方式1 继承Thread类 还是 方式2 实现Runnable接口这种方式多呢?答案是方式2
- 方式1的话有 Java单继承的局限性,因为继承了Thread类,就不能再继承其它的类了
- 方式2的共享资源的能力也会强一些,不需要非得加个static来修饰
Thread类 Runnable接口 有联系吗?
2.3.通过 Callable 和 Future 创建线程
对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式,都需要有一个run方法,但是这个run方法有不足:
- 没有返回值
- 不能抛出异常
基于上面的两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口:
- 实现Callable接口优点:(1)有返回值 (2)能抛出异常
- 缺点:线程创建比较麻烦
创建的过程说明如下:
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
案例如下:
public class TestRandomNum implements Callable<Integer> {
/*
1.实现callable接口,可以不带泛型,如果不带泛型,那么call方式的返回值就是object类型
2.如果带泛型,那么call的返回值就是泛型对应的类型
3.call有返回值,可以抛出异常
*/
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);//返回10以内的随机数
}
}
测试代码如下:
public class Test04 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//定义一个线程对象
TestRandomNum testRandomNum = new TestRandomNum();
FutureTask futureTask = new FutureTask(testRandomNum);
Thread t = new Thread(futureTask);
//启动线程
t.start();
//获取线程得到的返回值
Object o = futureTask.get();
System.out.println(o);
}
}
2.4.创建线程的三种方式的对比
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
三、线程的生命周期
线程经过其生命周期的各个阶段如下图:、
新建(new Thread)
- 当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
- 例如:Thread t1=new Thread();
就绪(runnable)
- 线程已经被启动,正在等待被分配给 CPU 时间片,也就是说此时线程正在就绪队列中排队等候得到 CPU 资源。
- 例如:t1.start();
运行(running)
- 线程获得 CPU 资源正在执行任务( run() 方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。
堵塞(blocked)由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
- 正在睡眠:用 sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
- 正在等待:调用 wait() 方法。(调用 motify() 方法回到就绪状态)
- 被另一个线程所阻塞:调用 suspend() 方法。(调用 resume() 方法恢复)
死亡(dead)当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
- 自然终止:正常运行 run() 方法后终止
- 异常终止:调用 stop() 方法让一个线程终止运行
四、线程的常用方法
4.1.设置优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 优先级在 MIN_PRIORITY(1)和 MAX_PRIORITY(10)之间的范围内。默认情况下,每一个线程都会分配一个优先级NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器时间。然而,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
- 同优先级别的线程,采取的策略就是先到先服务,使用时间片策略
- 如果优先级别高,被CPU调度的概率就高
- 级别:1-10 默认的级别为5
代码如下:
public class TestThread01 extends Thread{
public TestThread01(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName()+"产生:"+i);
}
}
}
class TestThread02 extends Thread{
public TestThread02(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName()+"产生:"+i);
}
}
}
class Test05{
//只是main方法,程序的入口
public static void main(String[] args) {
//创建两个子线程,让两个子线程抢夺资源
TestThread01 t1 = new TestThread01("子线程1");
//设置线程优先级
t1.setPriority(1);//优先级别低
t1.start();
TestThread02 t2 = new TestThread02("子线程2");
t2.setPriority(10);//优先级别高
t2.start();
}
}
4.2.sleep
sleep: 人为的制造阻塞事件,完成秒表功能:
public class Test07 {
public static void main(String[] args) {
//定义一个时间格式
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
while (true){
//1.获取当前时间
Date date = new Date();
//2.安装之前的格式对时间进行转换
System.out.println(sdf.format(date));
try {
//这里但是为毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.3.join方法
join方法:当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。注意:必须先start,再join才有效。
public class TestThread03 extends Thread{
public TestThread03(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName()+"产生的数据为:"+i);
}
}
}
class Test06{
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
System.out.println("main-----"+i);
if(i==60){
//如果等于6就创建子线程
TestThread03 t1 = new TestThread03("子线程1");
//启动线程
t1.start();
//设置join方法
t1.join();//设置之后,只有等子线程t1执行结束,主线程才会继续往后执行
}
}
}
}
4.4.setDaemon设置伴随线程
将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了
public class TestThread04 extends Thread{
public TestThread04(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(this.getName()+"产生了:"+i);
}
}
}
class Test08{
public static void main(String[] args) {
//创建子线程
TestThread04 t = new TestThread04("子线程1");
//设置伴随线程,先设置,在启动,注意顺序
t.setDaemon(true);
//启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程main方法产生:"+i);
}
}
}
4.5.stop
public class Test10 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
if(i == 6){
//结束线程
Thread.currentThread().stop();//过期方法,不建议使用
}
System.out.println(i);
}
}
}
五、线程安全问题
5.1.同步代码块
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。案例如下:
public class Test10 {
public static void main(String[] args) throws InterruptedException {
AddThread t1 = new AddThread("线程1");
DecThread t2 = new DecThread("线程2");
//启动线程
t1.start();
t2.start();
//设置join
t1.join();
t2.join();
//设想的值是0才对
System.out.println(Counter.count);
}
}
class Counter{
//共享变量
public static int count = 0;
}
class AddThread extends Thread{
public AddThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i < 10001; i++) {
//循环一次给属性添加1
Counter.count += 1;
}
}
}
class DecThread extends Thread{
public DecThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i < 10001; i++) {
//循环一次给属性添加1
Counter.count -= 1;
}
}
}
上面的代码两个线程同时对一个int
变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
上面的问题通过加锁和解锁的操作,将共享的数据保护起来,只能同一时间被一个线程操作。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized
关键字对一个对象进行加锁,语法如下
synchronized(Counter.lock) { // 获取锁
...
} // 释放锁
上面代码表示用Counter.lock
实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }
代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized
语句块结束会自动释放锁。这样一来,对Counter.count
变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。
使用synchronized
解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized
代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized
会降低程序的执行效率。
synchronized使用流程如下
:
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用
synchronized(lockObject) { ... }
。
在使用synchronized
的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized
结束处正确释放锁:
同时要注意,多个线程要使用同一把锁才可以,如果使用的是不同的锁,这个达不到效果,案例代码如下:
public class Test10 {
public static void main(String[] args) throws InterruptedException {
AddThread t1 = new AddThread("线程1");
DecThread t2 = new DecThread("线程2");
//启动线程
t1.start();
t2.start();
//设置join
t1.join();
t2.join();
//设想的值是0才对
System.out.println(Counter.count);
}
}
class Counter{
//申请一把锁
public static final Object Lock = new Object();
//共享变量
public static int count = 0;
}
class AddThread extends Thread{
public AddThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i < 10001; i++) {
synchronized (Counter.Lock){
//循环一次给属性添加1
Counter.count += 1;
}
}
}
}
class DecThread extends Thread{
public DecThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i < 10001; i++) {
synchronized (Counter.Lock){
//循环一次给属性添加1
Counter.count -= 1;
}
}
}
}
代码执行结果如下:
5.2.同步方法
Java程序依靠synchronized
对线程进行同步,使用synchronized
的时候,锁住的是哪个对象非常重要。让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized
逻辑封装起来,即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。代码如:
public synchronized void save(){}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类,案例之前讲过的抢火车票的案例也是存在线程安全问题:
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name) {
super(name);
}
//定义车票数量
static int ticketNum = 10;//多个对象共享10张机票
public BuyTicketThread() {
}
//每个窗口都是一个线程对象,每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票
for (int i = 0; i < 100; i++) {
if(ticketNum> 0){
//判断票数
System.out.println("我在:"+this.getName()+"买到了从北京到丽江的第:"+ticketNum-- +"张车票");
}
}
}
}
class Test13{
public static void main(String[] args) {
BuyTicketThread t1 = new BuyTicketThread("子线程1");
t1.start();
BuyTicketThread t2 = new BuyTicketThread("子线程2");
t2.start();
BuyTicketThread t3 = new BuyTicketThread("子线程3");
t3.start();
}
}
代码执行后,会发现10号车票被购买了两次(多执行几次,这个是系统调度CPU来实现,不一定每次都出现),明显不符合要求如下:
那么上面的问题就可以采用同步方法解决,代码如下:
- 静态同步方法的同步监视器是 类名.class 字节码信息对象 BuyTicketThread.class
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name) {
super(name);
}
//定义车票数量
static int ticketNum = 10;//多个对象共享10张机票
//每个窗口都是一个线程对象,每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票
for (int i = 0; i < 100; i++) {
if(ticketNum> 0){
//判断票数
System.out.println("我在:"+this.getName()+"买到了从北京到丽江的第:"+ticketNum-- +"张车票");
}
//调用同步方法
buyTicket();
}
}
//同步方法
public static synchronized void buyTicket(){//锁住的 同步监视器: BuyTicketThread.class
if(ticketNum>0){
System.out.println("我在:"+Thread.currentThread().getName()+"买到了从北京到丽江的第:"+ticketNum-- +"张车票");
}
}
}
- 非静态同步方法的同步监视器是this(当前的对象,或者调用当前方法的对象)
public class BuyTicketThread implements Runnable{
//定义车票数量
static int ticketNum = 10;//多个对象共享10张机票
//每个窗口都是一个线程对象,每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票
for (int i = 0; i < 100; i++) {
/*if(ticketNum> 0){
//判断票数
System.out.println("我在:"+Thread.currentThread().getName()+"买到了从北京到丽江的第:"+ticketNum-- +"张车票");
}*/
//调用同步方法
buyTicket();
}
}
//同步方法
public synchronized void buyTicket(){//锁住的是this(当前的对象,或者调用当前对象的方法)
//锁住的是this
if(ticketNum>0){
System.out.println("我在:"+Thread.currentThread().getName()+"买到了从北京到丽江的第:"+ticketNum-- +"张车票");
}
}
}
class Test13{
public static void main(String[] args) {
//创建实现类对象
BuyTicketThread buyTicketThread = new BuyTicketThread();
//创建线程
Thread t1 = new Thread(buyTicketThread, "线程1");
t1.start();
Thread t2 = new Thread(buyTicketThread, "线程2");
t2.start();
Thread t3 = new Thread(buyTicketThread, "线程3");
t3.start();
}
总结:
- 多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。锁一般都是引用数据类型的。
- 非静态同步方法的同步监视器是this,静态同步方法的同步监视器是 类名.class 字节码信息对象
- 同步代码块的效率要高于同步方法,原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
- 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
5.3.Lock锁
从Java 5开始,引入了一个高级的处理并发的java.util.concurrent
包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。Java语言直接提供了synchronized
关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。java.util.concurrent.locks
包提供的ReentrantLock
用于替代synchronized
加锁,synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的呀。是虚拟机级别的。但是Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。
public class BuyTicketThread extends Thread{
public BuyTicketThread(String name) {
super(name);
}
//定义车票数量
static int ticketNum = 10;//多个对象共享10张机票
//申请一把锁
Lock lock = new ReentrantLock();
//每个窗口都是一个线程对象,每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票
for (int i = 0; i < 100; i++) {
//加锁
lock.lock();
//为了防止由于代码出现问题,导致死锁,所以处理一下
try {
if (ticketNum > 0) {
//判断票数
System.out.println("我在:" + this.getName() + "买到了从北京到丽江的第:" + ticketNum-- + "张车票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
//释放锁,放在finally中是无论代码中是否有问题,都释放该锁
lock.unlock();
}
}
}
}
class Test13{
public static void main(String[] args) {
BuyTicketThread t1 = new BuyTicketThread("子线程1");
t1.start();
BuyTicketThread t2 = new BuyTicketThread("子线程2");
t2.start();
BuyTicketThread t3 = new BuyTicketThread("子线程3");
t3.start();
}
}
5.4.Lock和synchronized的区别和先后顺序
Lock和synchronized的区别:
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
- Lock---->同步代码块(已经进入了方法体,分配了相应资源)----> 同步方法(在方法体之外)
5.5.线程同步的优缺点
对比:
- 线程安全,效率低
- 线程不安全,效率高
线程同步可能造成死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
六、线程通信问题
一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当我们需要多个线程之间相互协作的时候,就需要我们掌握Java线程的通信方式。
6.1 锁与同步
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)。
在我们的线程之间,有一个同步的概念。什么是同步呢,假如我们现在有2位正在抄暑假作业答案的同学:线程A和线程B。当他们正在抄的时候,老师突然来修改了一些答案,可能A和B最后写出的暑假作业就不一样。我们为了A,B能写出2本相同的暑假作业,我们就需要让老师先修改答案,然后A,B同学再抄。或者A,B同学先抄完,老师再修改答案。这就是线程A,线程B的线程同步。
可以以解释为:线程同步是线程之间按照一定的顺序执行。为了达到线程同步,我们可以使用锁来实现它。
先来看看一个无锁的程序:
public class NoneLock {
static class TreadA implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Tread A"+i);
}
}
}
static class TreadB implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Tread B"+i);
}
}
}
public static void main(String[] args) {
new Thread(new TreadA()).start();
new Thread(new TreadB()).start();
}
}
执行这个程序,你会在控制台看到,线程A和线程B各自独立工作,输出自己的打印值。如下是我的电脑上某一次运行的结果。每一次运行结果都会不一样。
那我现在有一个需求,我想等A先执行完之后,再由B去执行,怎么办呢?最简单的方式就是使用一个“对象锁”:
public class NoneLock {
//创建一个对象锁
private static Object lock = new Object();
static class TreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 1000; i++) {
System.out.println("Tread A"+i);
}
}
}
}
static class TreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 1000; i++) {
System.out.println("Tread B"+i);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new TreadA()).start();
Thread.sleep(10);
new Thread(new TreadB()).start();
}
}
这里声明了一个名字为lock
的对象锁。我们在ThreadA
和ThreadB
内需要同步的代码块里,都是用synchronized
关键字加上了同一个对象锁lock
。根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放lock
,线程B才能获得锁lock
。
上面在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁。因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。
6.2 等待/通知机制
上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。而等待/通知机制是另一种方式。
Java多线程的等待/通知机制是基于Object
类的wait()
方法和notify()
, notifyAll()
方法来实现的。
- notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。
前面我们讲到,一个锁同一时刻只能被一个线程持有。而假如线程A现在持有了一个锁lock
并开始执行,它可以使用lock.wait()
让自己进入等待状态。这个时候,lock
这个锁是被释放了的。
这时,线程B获得了lock
这个锁并开始执行,它可以在某一时刻,使用lock.notify()
,通知之前持有lock
锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。
需要注意的是,这个时候线程B并没有释放锁
lock
,除非线程B这个时候使用lock.wait()
释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock
锁。
public class WaitAndNotify {
//创建一个对象锁
private static final Object lock = new Object();
static class TreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 10; i++) {
try {
System.out.println("Tread A"+i);
lock.notify();//通知其他获取lock锁的线程开始执行,
lock.wait();//让自己进入等待状态
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
static class TreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 10; i++) {
try {
System.out.println("Tread B"+i);
lock.notify();//通知其他获取lock锁的线程开始执行,
lock.wait();//让自己进入等待状态
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new TreadA()).start();
Thread.sleep(10);
new Thread(new TreadB()).start();
}
}
输出结果如下:
在这个Demo里,线程A和线程B首先打印出自己需要的东西,然后使用notify()
方法叫醒另一个正在等待的线程,然后自己使用wait()
方法陷入等待并释放lock
锁。
需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。
6.3 信号量
基于volatile
关键字的自己实现的信号量通信。
volitile关键字能够保证内存的可见性,如果用volitile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。
比如想让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。我应该怎样实现呢?
public class Signal {
private static volatile int signal = 0;
static class ThreadA implements Runnable{
@Override
public void run() {
while (signal<11){
if(signal%2 == 0){
System.out.println("threadA:"+signal);
synchronized (this){
signal+=1;
}
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
while (signal<11){
if(signal%2 == 1){
System.out.println("threadB:"+signal);
synchronized (this){
signal += 1;
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
//Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
输出结果如下:
使用了一个volatile
变量signal
来实现了“信号量”的模型。这里需要注意的是,volatile
变量需要进行原子操作。signal+=
并不是一个原子操作,所以我们需要使用synchronized
给它“上锁”。这种实现方式并不一定高效
信号量的应用场景:
假如在一个停车场中,车位是公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。因为在这种场景下,多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量。其实JDK中提供的很多多线程通信工具类都是基于信号量模型的。
6.4 管道
管道是基于“管道流”的通信方式。JDK提供了PipedWriter
、 PipedReader
、 PipedOutputStream
、 PipedInputStream
。其中,前面两个是基于字符的,后面两个是基于字节流的。
public class Pipe {
static class ReaderThread implements Runnable{
//创建一个管道流
private PipedReader reader;
public ReaderThread(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
System.out.println("进行字符的读取:");
int n = 0;
try {
while ((n = reader.read()) != -1){
//转换后输出
System.out.print((char) n);
}
} catch (IOException ioException) {
ioException.printStackTrace();
}finally {
try {
reader.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
static class WriterThread implements Runnable{
//创建一个管道流
private PipedWriter writer ;
public WriterThread(PipedWriter writer) {
this.writer = writer;
}
@Override
public void run() {
System.out.println("进行字符的写入:");
int n = 0;
try {
writer.write("helloworld");
} catch (IOException ioException) {
ioException.printStackTrace();
}finally {
try {
writer.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException, IOException {
//创建管道输入、输出流
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
writer.connect(reader); // 这里注意一定要连接,才能通信
new Thread(new ReaderThread(reader)).start();
Thread.sleep(10);
new Thread(new WriterThread(writer)).start();
}
}
输出结果如下:
进行字符的读取:
进行字符的写入:
helloworld
我们通过线程的构造函数,传入了PipedWrite
和PipedReader
对象。可以简单分析一下这个示例代码的执行流程:
- 线程ReaderThread开始执行,
- 线程ReaderThread使用管道reader.read()进入”阻塞“,
- 线程WriterThread开始执行,
- 线程WriterThread用writer.write("test")往管道写入字符串,
- 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
- 线程ReaderThread接受到管道输出的字符串并打印,
- 线程ReaderThread执行完毕。
管道通信的应用场景:
使用管道多半与I/O流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了