【并发编程】线程基础知识

目录

目录

进程、线程、协程

并发与并行

上下文切换

Java 程序天生就是多线程的

线程的状态及其相互转换

线程的创建

1 、继承Thread类

2 、实现Runnable接口

3、实现Callable接口

4、线程池

5、【面试题】新启线程有几种方式?

6、深入理解run()和start()

线程的挂起恢复

线程的中止中断

中止

中断

线程的优先级

守护线程

synchronized

线程间的通信

volatile

wait、notify、notifyAll

join 方法

管道流



进程、线程、协程

进程是系统进行分配和管理资源的基本单位

线程进程的一个执行单元,是进程内调度的实体、是CPU调度和分派的基本单位,是比进程更小的独立运行的基本单位。线程也被称为轻量级进程,线程是程序执行的最小单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程协程是一种用户态的轻量级线程,协程的调度完全由用户控制。从技术的角度来说,“协程就是你可以暂停执行的函数”。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。Java中的协程详细说明

并发与并行

我们举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车 辆就是 8 辆此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可 以并行运行。 CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线 程数就相当于并排可以通行的车道;而多个 CPU 就相当于并排有多条高速公路,而 每个高速公路并排有多个车道。

当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少? 离开了单位时间其实是没有意义的。

综合来说:

并发 Concurrent:指应用能够交替执行不同的任务, 比如单 CPU 核心下执行多 线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉 到的速度不断去切换这两个任务, 已达到" 同时执行效果",其实并不是的, 只是计算 机的速度太快,我们无法察觉到而已.

并行 Parallel:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边 打电话,这两件事情可以同时执行

两者区别:一个是交替执行,一个是同时执行,如下图所示。

上下文切换

cpu为线程分配时间片,时间片非常短(毫秒级别),cpu不停的切换线程执行,在切换前会保存上一个任务 的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行的。

上下文的频繁切换,会带来一定的性能开销,如何减少上下文切换的开销?

无锁并发编程

  • 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用 锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据

CAS

  • Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

使用最少线程

  • 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态

协程

  • 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。--GO

Java 程序天生就是多线程的

一个 Java 程序从 main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与, 但实际上 Java 程序天生就是多线程程序,因为执行 main() 方法的是一个名称为 main 的线程。

/**
 *类说明:只有一个main方法的程序
 */
public class OnlyMain {
    public static void main(String[] args) {
        //Java 虚拟机线程系统的管理接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos =
                threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] "
                    + threadInfo.getThreadName());
        }
    }
}

而一个 Java 程序的运行就算是没有用户自己开启的线程,实际也有很多 JVM 自行启动的线程, 执行上述Main方法,打印线程信息:

[6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的

[5] Attach Listener //内存 dump,线程 dump,类信息统计, 获取系统属性等 [4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程

[3] Finalizer // 调用对象 finalize 方法的线程

[2] Reference Handler//清除 Reference 的线程

[1] main //main 线程, 用户程序入口

尽管这些线程根据不同的 JDK 版本会有差异, 但是依然证明了 Java 程序天生就是多线程的。

线程的状态及其相互转换

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(RUNNABLE):处于可运行状态的线程正在JVM中执行,但它可能正在等待来自操作系统的其他资源,例 如处理器。
  • 阻塞(BLOCKED):线程阻塞于synchronized锁,等待获取synchronized锁的状态。
  • 等待(WAITING):Object.wait()、join()、 LockSupport.park(),进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(TIME_WAITING):Object.wait(long)、Thread.join()、LockSupport.parkNanos()、 LockSupport.parkUntil,该状态不同于WAITING,它可以在指定的时间内自行返回。
  • 终止(TERMINATED):表示该线程已经执行完毕。

线程的创建

创建线程的方式有:

1 、继承Thread类

继承Thread,重写run方法

public class ThreadDemo {
    /*扩展自Thread类*/
    private static class UseThread extends Thread {
        @Override
        public void run() {
            // do my work;
            System.out.println("I am extendec Thread");
        }
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        UseThread useThread = new UseThread();
        useThread.start();
        System.out.println("main end");
    }
}

2 、实现Runnable接口

实现Runnable接口,重写run方法,无返回值

public class RunnableDemo{
	/*实现Runnable接口*/
	private static class UseRunnable implements Runnable{
		@Override
		public void run() {
			// do my work;
			System.out.println("I am implements Runnable");
		}
	}
    
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		UseRunnable useRunnable = new UseRunnable();
		new Thread(useRunnable).start();
		System.out.println("main end");
	}
}

3、实现Callable接口

 实现Callable接口,重写call方法,有返回值

public class CallableDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        // do my work;
        System.out.println("I am implements Callable");
        return "Callable end";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableDemo callableDemo = new CallableDemo();
        FutureTask<String> stringFutureTask = new FutureTask<>(callableDemo);
        new Thread(stringFutureTask).start();
        System.out.println(stringFutureTask.get());
		System.out.println("main end");
    }
}

Callable详解

Callable 位于 java.util.concurrent 包下, 它也是一个接口, 在它里面也只声明了一个方法,只不过这个方法叫做 call() ,这是一个泛型接口,call()函数返回的类型就是传递进来的 V 类型。

Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get() 方法获取执行结果,该方法会阻塞直到任务返回结果。

因为 Future 只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的 FutureTask。

 

FutureTask 类实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口和 Future 接口, 而 FutureTask 实现了 RunnableFuture 接口。所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

因此我们通过一个线程运行 Callable,但是 Thread 不支持构造方法中传递 Callable 的实例,所以我们需要通过 FutureTask 把一个 Callable 包装成 Runnable, 然后再通过这个 FutureTask 拿到 Callable 运行后的返回值。

要 new 一个 FutureTask 的实例,有两种方法

4、线程池

5、【面试题】新启线程有几种方式?

这个问题的答案其实众说纷纭,有 2 种, 3 种, 4 种等等答案,建议比较好的回答是:

按照 Java 源码中 Thread 上的注释:

官方说法是在 Java 中有两种方式创建一个线程用以执行, 一种是派生自 Thread 类,另一种是实现 Runnable 接口。

当然本质上 Java 中实现线程只有一种方式, 都是通过 new Thread()创建线程对象,调用 Thread#start 启动线程。

至于基于 callable 接口的方式,因为最终是要把实现了 callable 接口的对象,通过 FutureTask 包装成 Runnable,再交给 Thread 去执行,所以这个其实可以和实现 Runnable 接口看成同一类。

而线程池的方式,本质上是池化技术,是资源的复用,和新启线程没什么关系,ThreadPoolExecutor底层线程对象也是实现Runable接口。

所以,比较赞同官方的说法,有两种方式创建一个线程用以执行。

6、深入理解run()和start()

Thread 类是Java 里对线程概念的抽象,可以这样理解:我们通过 new Thread() 其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了 start()方法后,才实现了真正意义上的启动线程。

从 Thread 的源码可以看到,Thread 的 start 方法中调用了 start0()方法,而 start0()是个 native 方法, 这就说明 Thread#start 一定和操作系统是密切相关的。

start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常(注意, 此处可能有面试题:多次调用一个线程的 start 方法会怎么样? )。

而 run 方法是业务逻辑实现的地方, 本质上和任意一个类的任意一个成员方法并没有任何区别, 可以重复执行,也可以被单独调用。

线程的挂起恢复

什么是挂起线程?

  • 线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程 时间片,进入这个状态可以用来暂停一个线程的运行。 在线程挂起后,可以通过重新唤醒线程来使之恢复运行

为什么要挂起线程?

  • cpu分配的时间片非常短、同时也非常珍贵。避免资源的浪费。

如何挂起线程?

  • 被废弃的方法 thread.suspend() 该方法不会释放线程所占用的资源。如果使用该方法将某个线程挂起,则可能会使其他等待资源的线程死锁
  • thread.resume() 方法本身并无问题,但是不能独立于suspend()方法存在
  • suspend() 、resume() 和 stop() 这些 API 是过期的,也就是不建议使用的
  • 可以使用的方法 wait() 暂停执行、放弃已经获得的锁、进入等待状态 ,notify() 随机唤醒一个在等待锁的线程 ,notifyAll() 唤醒所有在等待锁的线程,自行抢占cpu资源

什么时候适合使用挂起线程?

  • 我等的船还不来(等待某些未就绪的资源),我等的人还不明白。直到notify方法被调用

线程的中止中断

中止

线程自然终止

要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

stop()

暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()resume()stop()。但是这些 API 是过期的,也就是不建议使用的。

不建议使用的原因主要有:

以 suspend() 方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

同样,stop() 方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend() 、 resume() 和 stop() 方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

中断

安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表 线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。

线程通过检查自身的中断标志位是否被置为 true 来进行响应,

线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。

如果一个线程处于了阻塞状态(如线程调用了 thread.sleep 、thread.join、

thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。

不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,

        一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,

        二、检查中断位的状态和检查取消标志位没什么区别, 用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

/**
 * 类说明:如何安全中断线程
 * 测试 isInterrupted() Thread.interrupted()
 */
public class EndThread {
	
	private static class UseThread extends Thread{

		private boolean cancel; //不建议自定义一个取消标志位来中止线程的运行

		public UseThread(String name) {
			super(name);
		}

		public void setCancel(boolean cancel) {
			this.cancel = cancel;
		}

		@Override
		public void run() {
			String threadName = Thread.currentThread().getName();
			System.out.println(threadName+" interrrupt flag ="+isInterrupted());
			//while(!isInterrupted()){
				//Thread.sleep();

			//while(!Thread.interrupted()){
			while(true){
				System.out.println(threadName+" is running");
				System.out.println(threadName+"inner interrrupt flag ="
						+isInterrupted());
			}
			//System.out.println(threadName+" interrrupt flag ="+isInterrupted());
		}
	}

	public static void main(String[] args) throws InterruptedException {
		UseThread endThread = new UseThread("endThread");
		endThread.start();
		Thread.sleep(20);
		endThread.interrupt();//中断线程,其实设置线程的中断标识位=true
	}

}

线程的优先级

线程的优先级告诉程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,程序会尽可能地先运行优先级的那个线程。 但是,这并不表示优先级较低的线程不会运行。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。

线程的优先级设置可以为1-10的任一数值,Thread类中定义了三个线程优先级,分别是: MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),一般情况下推荐使用这几个常量,不要自行设置数值。

不同平台,对线程的优先级的支持不同。 编程的时候,不要过度依赖线程优先级,如果你的程序运行是否正确取决于你设置的优先级是否按所设置的优先级运行,那这样的程序不正确

任务: 快速处理-->设置高的优先级  慢慢处理-->设置低的优先级

/**
 * 线程优先级Demo
 */
public class PriorityDemo {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "线程1");

        Thread thread2 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "线程2");

        thread.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);

        thread.start();
        thread2.start();
    }
}

守护线程

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的时候, Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。

Daemon 线程被用作完成支持性工作, 但是在 Java 虚拟机退出时 Daemon 线 程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。

建议: 尽量少使用守护线程,因其不可控不要在守护线程里去进行读写操作、执行计算逻辑

/**
 * 守护线程Demo
 */
public class DaemonThreadDemo implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new DaemonThreadDemo());
        thread.start();
        thread.setDaemon(true); // 设置成守护线程 main线程运行结束 程序退出
        Thread.sleep(2000L);
    }
}

synchronized

内置锁

每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获 得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块 或方法。

互斥锁

内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

修饰普通方法:锁住对象的实例

修饰静态方法:锁住整个类

修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容

对象锁和类锁

对象锁是用于对象实例方法, 或者一个对象实例上的, 类锁是用于类的静态方法或者一个类的 class 对象上的。

比如上面的 synClass 方法就使用了类锁。

我们知道, 类的对象实例可以有很多个, 所以当对同一个变量操作时, 用来做锁的对象必须是同一个,否则加锁毫无作用。 比如下面的示例代码:

但是有一点必须注意的是, 其实类锁只是一个概念上的东西, 并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象,但是每个类只有一个 class 对 象,所以每个类只有一个类锁。

同样的,当对同一个变量操作时,类锁和对象(非 class 对象)锁混用也同样毫无用处。

错误的加锁和原因分析

参见代码

public class TestIntegerSyn {

    public static void main(String[] args) throws InterruptedException {
        Worker worker=new Worker(1);
        //Thread.sleep(50);
        for(int i=0;i<5;i++) {
            new Thread(worker).start();
        }
    }

    private static class Worker implements Runnable{

        private Integer i;
        private Object o = new Object();

        public Worker(Integer i) {
            this.i=i;
        }

        @Override
        public void run() {
            synchronized (i) {
                Thread thread=Thread.currentThread();
                System.out.println(thread.getName()+"--@"
                        +System.identityHashCode(i));
                i++;
                System.out.println(thread.getName()+"-------[i="+i+"]-@"
                        +System.identityHashCode(i));
                SleepTools.ms(3000);
                System.out.println(thread.getName()+"-------[i="+i+"]--@"
                        +System.identityHashCode(i));
            }

        }

    }

}

执行结果

可以看到 i 的取值会出现乱序或者重复取值的现象

原因:虽然我们对 i 进行了加锁,但是

但是当我们反编译这个类的 class 文件后,可以看到 i++实际是,

本质上是返回了一个新的 Integer 对象。也就是每个线程实际加锁的是不同 的 Integer 对象,所以说到底, 还是当对同一个变量操作时, 用来做锁的对象必 须是同一个,否则加锁毫无作用。

线程间的通信

volatile

最轻量的通信/同步机制(不是锁)

volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某 个变量的值,这新值对其他线程来说是立即可见的。

不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环, 而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环。

但是 volatile 不能保证数据在多个线程下同时写时的线程安全,volatile 最适用的 场景:一个线程写,多个线程读。

演示Volatile的提供的可见性

public class VolatileCase {
    private static boolean ready;

    // 使用volatile
    // private static volatile boolean ready;
    private static int number;

    private static class PrintThread extends Thread{
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while(!ready){
                 // System.out.println("lll");
                 // 有的同学发现当我们执行System.out.println("lll")打印时,循环也退出了,是因为println方法使用了synchronized关键字
            };//无限循环
            System.out.println("number = "+number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new PrintThread().start();
        SleepTools.second(1);
        number = 51;
        ready = true;
        SleepTools.second(5);
        System.out.println("main is ended!");
    }
}

wait、notify、notifyAll

等待方遵循如下原则

1)获取对象的锁。

2)如果条件不满足, 那么调用对象的 wait()方法, 被通知后仍要检查条件。

3)条件满足则执行对应的逻辑。

通知方遵循如下原则

1)获得对象的锁。

2)改变条件。

3)通知所有等待在对象上的线程。

在调用 wait()、notify()系列方法之前, 线程必须要获得该对象的对象级别锁, 即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进 入 wait() 方法后, 当前线程释放锁,在从 wait()返回前,线程与其他线程竞争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized 代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁, 它就会继续往下执行, 在它退出 synchronized 代码块,释放锁后, 其他的已经被唤醒的线程将会继续竞争获取该锁, 一直进行下去, 直到所有被唤醒的线程都执行完毕。

notify 和notifyAll 应该用谁?

尽可能用 notifyAll(),谨慎使用 notify() ,因为 notify()只会唤醒一个线程, 我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

方法和锁

调用 yield() 、sleep() 、wait() 、notify()等方法对锁有何影响?

(1)yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。

(2)调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后, 会重新去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。

(3)调用 notify()系列方法后, 对锁无影响, 线程只有在 syn 同步代码执行完后才会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行。

为什么 wait 和 notify 方法要在同步块中调用?

原因

主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。 其实真实原因是:

这个问题并不是说只在 Java 语言中会出现,而是会在所有的多线程环境下出现。

假如我们有两个线程, 一个消费者线程, 一个生产者线程。生产者线程的任务可以简化成将 count 加一,而后唤醒消费者; 消费者则是将 count 减一,而后在减到 0 的时候陷入睡眠:

生产者伪代码:

count+1;

notify();

消费者伪代码:

while(count<=0)

        wait()

count--

这里面有问题。什么问题呢?

生产者是两个步骤:

1. count+1;

2. notify();

消费者也是两个步骤:

1. 检查 count 值;

2. 睡眠或者减一;

万一这些步骤混杂在一起呢?比如说,初始的时候 count 等于 0,这个时候消费者检查 count 的值,发现 count 小于等于 0 的条件成立; 就在这个时候, 发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了, 也就是发出了通知, 准备唤醒一个线程。这个时候消费者刚决定睡觉, 还没睡呢, 所以这个通知就会被丢掉。紧接着,消费者就睡过去了……

这就是所谓的 lost wake up 问题。

那么怎么解决这个问题呢?

现在我们应该就能够看到,问题的根源在于,消费者在检查 count 到调用 wait()之间, count 就可能被改掉了。

这就是一种很常见的竞态条件。

很自然的想法是, 让消费者和生产者竞争一把锁, 竞争到了的, 才能够修改 count 的值。

join 方法

面试题:现在有 T1、T2、T3 三个线程, 你怎样保证 T2 在 T1 执行完后执行, T3 在 T2 执行完后执行?

答:用 Thread#join 方法即可, 在 T3 中调用 T2.join,在 T2 中调用 T1.join。

join():把指定的线程加入到当前线程, 可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法, 直到线程 A 执行完毕后, 才会继续执行线程 B 剩下的代码。

演示无Join时线程的表现

public class NoUseJoin {
	
    static class Goddess implements Runnable {
        private Thread thread;

        public Goddess(Thread thread) {
            this.thread = thread;
        }

        public Goddess() {
        }

        public void run() {
            System.out.println("Goddess开始排队打饭.....");
            try {
                if(thread!=null) thread.join();
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName()
                    + " Goddess打饭完成.");
        }
    }

    static class GoddessBoyfriend implements Runnable {

        public void run() {
            System.out.println("GoddessBoyfriend开始排队打饭.....");
            System.out.println(Thread.currentThread().getName()
                    + " GoddessBoyfriend打饭完成.");
        }
    }

    public static void main(String[] args) throws Exception {

        Thread zhuGe = Thread.currentThread();
        GoddessBoyfriend goddessBoyfriend = new GoddessBoyfriend();
        Thread gbf = new Thread(goddessBoyfriend);
        Goddess goddess = new Goddess();
        Thread g = new Thread(goddess);
        gbf.start();
        g.start();
        System.out.println("zhuGe开始排队打饭.....");
        //SleepTools.second(2);//让主线程休眠2秒
        System.out.println(zhuGe.getName() + " zhuGe打饭完成.");
    }
}


演示Join()方法的使用

public class UseJoin {
	
    static class Goddess implements Runnable {
        private Thread thread;

        public Goddess(Thread thread) {
            this.thread = thread;
        }

        public Goddess() {
        }

        public void run() {
            System.out.println("Goddess开始排队打饭.....");
            try {
                if(thread!=null) thread.join();
            } catch (InterruptedException e) {
            }
            SleepTools.second(2);//休眠2秒
            System.out.println(Thread.currentThread().getName()
                    + " Goddess打饭完成.");
        }
    }

    static class GoddessBoyfriend implements Runnable {

        public void run() {
            SleepTools.second(2);//休眠2秒
            System.out.println("GoddessBoyfriend开始排队打饭.....");
            System.out.println(Thread.currentThread().getName()
                    + " GoddessBoyfriend打饭完成.");
        }
    }

    public static void main(String[] args) throws Exception {

        Thread zhuGe = Thread.currentThread();
        GoddessBoyfriend goddessBoyfriend = new GoddessBoyfriend();
        Thread gbf = new Thread(goddessBoyfriend);
        Goddess goddess = new Goddess(gbf);
        Thread g = new Thread(goddess);
        g.start();
        gbf.start();
        System.out.println("zhuGe开始排队打饭.....");
        g.join();
        SleepTools.second(2);//让主线程休眠2秒
        System.out.println(zhuGe.getName() + " zhuGe打饭完成.");
    }
}

管道流

以内存为媒介,用于线程之间的数据传输。

主要有面向字节:【PipedOutputStream、PipedInputStream】、面向字符【PipedReader、PipedWriter】

public class Piped {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        /* 将输出流和输入流进行连接,否则在使用时会抛出IOException*/
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        try {
            /*将键盘的输入,用输出流接受,在实际的业务中,可以将文件流导给输出流*/
            while ((receive = System.in.read()) != -1){
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }

    static class Print implements Runnable {
        private PipedReader in;
        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                /*输入流从输出流接收数据,并在控制台显示
                *在实际的业务中,可以将输入流直接通过网络通信写出 */
                while ((receive = in.read()) != -1){
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值