目录
8.2 Synchronizing Statement (同步语句)
1、分享的目的:
进一步掌握多线程编程和应用的技巧,对在平时的开发中应对高并发编程有所帮助。
首先理解并行和并发的区别:
并行:指在同一时刻,有多条指令在多个处理器上同时执行;
并发:指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果
2、使用多线程的意义
1)提高资源利用率:一个程序作为一个进程来运行, 程序运行过程中能够创建多个线程, 而一个线程在一个时刻只能运行在一个处理器核心上。
2)程序设计在某些情况下更简单;
3)程序响应更快。
4)避免阻塞(异步调用) :单个线程中的程序,是顺序执行的。如果前面的操作发生了阻塞,那么就会影响到后面的操作。这时候可以采用多线程,我感觉就等于是异步调用
多线程的代价:
1)设计更复杂
虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且重现以修复。
2)上下文切换的开销
当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切换并不廉价。如果没有必要,应该减少上下文切换的发生。
3、线程和进程的概念
操作系统调度的最小单元是线程, 也叫轻量级进程(Light Weight Process) , 在一个进程里可以创建多个线程, 这些线程都拥有各自的计数器、 堆栈和局部变量等属性, 并且能够访问共享的内存变量。 处理器在这些线程上高速切换, 让使用者感觉到这些线程在同时执行。
进程:每个进程都有独立的代码和数据空间(进程上下文),独立的一块内存空间,进程间的切换会有较大的开销,一个进程包含1--n个线程。
线程:线程是指进程中的一个执行流程(顺序执行流),同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。进程中的多个线程共享进程的内存。线程是进程的组成部分
- 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
- 多进程是指操作系统能同时运行多个任务(程序)。
- 多线程是指在同一程序中有多个顺序流在执行。
- 同一时间,同一个核心,只可能有一个线程运行,其他处理挂起的状态。
- 进程是指一个内存中运行的应用程序
- 一个进程中可以运行多个线程,线程总是属于某个进程,进程中的多个线程共享进程的内存。
- 一个线程不能独立的存在,它必须是进程的一部分。
一个Java程序从main()方法开始执行, 然后按照既定的代码逻辑执行, 看似没有其他线程参与, 但实际上Java程序天生就是多线程程序, 因为执行main()方法的是一个名 称为main的线程。 下面使用JMX来查看一个普通的Java程序包含哪些线程, 代码如下:
public class MultiThread{
public static void main(String[ ] args) {
// 获取Java线程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory. getThreadMXBean() ;
// 不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
ThreadInfo[ ] threadInfos =
threadMXBean.dumpAllThreads(false, false) ;
// 遍历线程信息,仅打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System. out. println("[ " + threadInfo. getThreadId() +
"] " + threadInfo.getThreadName()) ;
}
}
}
输出结果如下(多次运行,结果可能不同):
[4] Signal Dispatcher //分发处理发送给JVM信号的线程
[3] Finalizer //调用对象finalize方法的线程
[2] Reference Handler //清除Reference的线程
[1] main //main线程,用户程序入口
3.1、进程和线程的区别
3.1.1、进程的特性
1) 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,其他进程不能访问这个进程空间内的数据。
2) 动态性:进程与程序的区别在于,程序是静态的,进程是动态的,程序只是一个静态的指令集合,而进程是一个正在系统中运行的指令集合,有生命周期等时间概念;
3) 并发性:进程之间,可以交替执行,提高程序执行效率。
3.1.2、线程的特性
1) 进程之间不能共享内存,但线程可以共享同一片内存中的数据;
2) 系统创建进程需要为该进程重新分配系统资源,但创建线程的代价很小,因此用多线程实现多任务并发比多进程实现并发的效率高;
3) java语言内置多线程功能支持,而不是单纯的作为底层操作系统的调度方式,Java封装了操作系统底层的调度,屏蔽了不同操作系统调度之间的差异。
4、线程的生命周期
- 新建状态(New): 一个新产生的线程从新状态开始了它的生命周期。它保持这个状态直到程序start这个线程。
- 就绪状态(Ready):用户调用 start() 方法之后,该线程就进入就绪状态,但不是处于可运行状态。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 等待状态(wait):当处于运行状态下的线程调用 Thread 类的 wait() 方法时,该线程就会进入等待状态。进入等待状态的线程必须调用 Thread 类的 notify() 方法才能被唤醒。notifyAll() 方法是将所有处于等待状态下的线程唤醒。
- 休眠()状态(Blocked): 由于一个线程的时间片用完了,该线程从运行状态进入休眠状态。当时间间隔到期或者等待的事件发生了,该状态的线程切换到运行状态。或者当线程调用 Thread 类中的 sleep() 方法时,则会进入休眠状态。
- 阻塞状态:如果一个线程在运行状态下发出输入/输出请求,该线程将进入阻塞状态,在其等待输入/输出结束时,线程进入就绪状态。对阻塞的线程来说,即使系统资源关闭,线程依然不能回到运行状态。
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中(wait会释放持有的锁)。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态(注意,sleep不会释放线程所持有的锁)。
- 死亡状态(Dead): 一个运行状态的线程完成任务或者其他终止条件发生,该线程就切换到终止状态或者当线程的 run() 方法执行完毕,线程进入死亡状态。
使线程处于就绪状态有如下几种方法。
- 调用 sleep() 方法。
- 调用 wait() 方法。
- 等待输入和输出完成。
当线程处于就绪状态后,可以用如下几种方法使线程再次进入运行状态。
- 线程调用 notify() 方法。
- 线程调用 notifyAll() 方法。
- 线程调用 intermpt() 方法。
- 线程的休眠时间结束。
- 输入或者输出结束
5、线程的优先级
Java给每个线程安排优先级以决定与其他线程比较时该如何对待该线程。线程的优先级是用来决定何时从一个运行的线程切换到另一个。这叫“上下文转换”(context switch)。决定上下文转换发生的规则很简单:
- 线程可以自动放弃控制。在I/O未决定的情况下,睡眠或阻塞由明确的让步来完成。在这种假定下,所有其他的线程被检测,准备运行的最高优先级线程被授予CPU。
- 线程可以被高优先级的线程抢占。在这种情况下,低优先级线程不主动放弃,处理器只是被先占——无论它正在干什么——处理器被高优先级的线程占据。基本上,一旦高优先级线程要运行,它就执行。这叫做有优先权的多任务处理。
每一个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。通过一个整型成员变量priority来控制优先级, 优先级的范围从1~10, 在线程构建的时候可以通过setPriority(int)方法来修改优先级, 默认优先级是5, 优先级高的线程分配时间片的数量要多于优先级低的线程。
public final void setPriority(int newPriority);
如果要获取当前线程的优先级,可以直接调用 getPriority() 方法。语法如下:
public final int getPriority();
然而,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5
6、线程的创建方法
6.1、实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;
public class TaskClass implements Runnable {
public TaskClass(...) {
}
// 实现Runnable中的run方法
public void run() {
// 告诉系统如何运行自定义线程
}
}
public class Client {
public void someMethod() {
// 创建TaskClass的实例
TaskClass task = new TaskClass(...);
// 创建线程
Thread thread = new Thread(task);
// 启动线程
thread.start();
}
}
通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
6.2、直接继承Thread类并重写run方法,Thread实现Runnable,和实现Runnable原理一致,本质上也是实现了 Runnable 接口的一个实例。(不推荐使用:Java 不支持多继承)
// 自定义thread类
public class CustomThread extends Thread {
public CustomThread(...) {
}
// 重写Runnable里的run方法
public void run() {
// 告诉系统如何执行这个task
}
}
// 自定义类
public class Client {
public void someMethod() {
// 创建一个线程
CustomThread thread1 = new CustomThread(...);
// 启动线程
thread1.start();
// 创建另一个线程
CustomThread thread2 = new CustomThread(...);
// 启动线程
thread2.start();
}
}
6.3、Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
注意:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
7、线程池
线程池用于高效地执行任务
如果需要为一个任务创建一个线程,那么用Thread类,如果有多个任务,最好用线程池,否则就要为逐个任务创建线程,这种做法会导致低吞吐量和低性能。
Executor 接口用于在线程池中执行线程,ExecutorService 子接口用于管理和控制线程。
isTerminated() : 如果线程池中所有的任务都已终止,则返回true。
1. 使用 Executor 类中的静态方法创建 Executor 对象。
2. newFixedThreadPool(int) 方法在池中创建固定数目的线程。如果一个线程结束执行一个任务,则可以重用来执行另一个任务。
3. 如果一个线程在shutdown前失败,并且池中所有线程非idle状态,还有新的任务等待执行,那么新的线程会被创建以取代出错的线程。
4. 如果池中所有线程非idle状态,并且还有新的任务等待执行,newCachedThreadPool() 方法将用于创建新的线程。
5. 如果缓冲池中的线程超过60秒未被使用,则会被终止。缓冲池用来执行数目众多的短任务时十分高效。
Executor并发执行3个线程:
package testpackage;
import java.util.concurrent.*;
public class TaskThreadDemo {
public static void main(String[] args) {
// Create a fixed thread pool with maximum three threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit runnable tasks to the executor
executor.execute(new PrintChar('a', 100));
executor.execute(new PrintChar('b', 100));
executor.execute(new PrintNum(100));
// Shut down the executor
executor.shutdown();
}
}
// 打印指定次数字符的 task
class PrintChar implements Runnable {
private char charToPrint; // The character to print
private int times; // The number of times to repeat
/** Construct a task with a specified character and number of
* times to print the character
*/
public PrintChar(char c, int t) {
charToPrint = c;
times = t;
}
@Override /** Override the run() method to tell the system
* what task to perform
*/
public void run() {
for (int i = 0; i < times; i++) {
System.out.print(charToPrint);
}
}
}
// The task class for printing numbers from 1 to n for a given n
class PrintNum implements Runnable {
private int lastNum;
/** Construct a task for printing 1, 2, ..., n */
public PrintNum(int n) {
lastNum = n;
}
@Override /** Tell the thread how to run */
public void run() {
for (int i = 1; i <= lastNum; i++) {
System.out.print(" " + i);
}
}
}
如果将固定线程数由3改为1:
ExecutorService executor = Executors.newFixedThreadPool(1);
3个任务将顺序执行。如果改为数目不固定,3个task将并发执行。
ExecutorService executor = Executors.newCachedThreadPool();
shutdown()则命令 executor关闭,之后不再接受新的任务,但如果有存在的线程,那么这些线程将继续执行直到结束。
Java通过Executors提供四种线程池,分别为:
7.1、newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
7.2、newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
7.3、newScheduledThreadPool
创建一个大小无限制的线程池。此线程池支持定时以及周期性执行任务。
7.4、newSingleThreadExecutor
创建一个单线程的线程池。此线程池支持定时以及周期性执行任务。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(index);
}
});
}
}
}
8、线程同步
定义:当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用。达到此目的的过程叫做同步(synchronization)。
Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。
有四种不同的同步块:
- 实例方法
- 静态方法
- 实例方法中的同步块
- 静态方法中的同步块
线程同步是为了协调互相依赖的线程的执行。同步的关键是管程(也叫信号量semaphore)的概念。管程是一个互斥独占锁定的对象,或称互斥体(mutex)。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定,它必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其他的线程被称为等待管程。一个拥有管程的线程如果愿意的话可以再次进入相同的管程。
示例,创建100个线程,每个线程各往同一个银行账户里存1分钱,理论上全部线程执行完毕,账户结余应为100分,运行结果只有3分或其他不正确的结果等等。
package testpackage;
import java.util.concurrent.*;
public class TaskThreadDemo {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// Create and launch 100 threads
for (int i = 0; i < 100; i++) {
executor.execute(new AddAPennyTask());
}
executor.shutdown();
// Wait until all tasks are finished
while (!executor.isTerminated()) {
}
System.out.println("What is balance? " + account.getBalance());
}
// A thread for adding a penny to the account
private static class AddAPennyTask implements Runnable {
public void run() {
account.deposit(1);
}
}
// An inner class for account
private static class Account {
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
int newBalance = balance + amount;
// data-corruption problem and make it easy to see.
try {
Thread.sleep(5);
}
catch (InterruptedException ex) {
}
balance = newBalance;
}
}
}
出问题的是以下两条语句,这两条语句各个线程叠加访问,造成访问冲突,变量值没有同步. 这两条语句之间等待的时间越长,执行结果越不正确:
newBalance = balance + 1;
balance = newBalance;
8.1 使用synchronized关键字
为了解决上面的问题,一种方法是使用synchronized关键字, 对临界区加锁,执行完成后释放锁。
public synchronized void deposit(double amount)
同步的方法在执行前请求锁,如果是实例方法,对对象加锁。如果是静态方法,对类加锁, 方法执行结束后释放锁。
8.2 Synchronizing Statement (同步语句)
也可以只对语句块进行同步,语法:
synchronized (expr) {
statements;
}
expr 必须为对象的引用,上例中,相应语句修改如下,运行结果就是100.
synchronized (account) {
account.deposit(1);
}
比起对整个method加Synchronized, 这种改法能提高并发性。
以下两种写法等价:
1.
public synchronized void xMethod() {
// method body
}
2.
public void xMethod() {
synchronized (this) {
// method body
}
}
synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;
3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。
总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
在进一步阐述之前,我们需要明确几点:
A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
B.每个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
9、线程间通信
为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
- wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
- notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
- notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。
10、线程的死锁
需要避免的与多任务处理有关的特殊错误类型是死锁(deadlock)。死锁发生在当两个线程对一对同步对象有循环依赖关系时。例如,假定一个线程进入了对象X的管程而另一个线程进入了对象Y的管程。如果X的线程试图调用Y的同步方法,它将像预料的一样被锁定。而Y的线程同样希望调用X的一些同步方法,线程永远等待,因为为到达X,必须释放自己的Y的锁定以使第一个线程可以完成。死锁是很难调试的错误,因为:
- 通常,它极少发生,只有到两线程的时间段刚好符合时才能发生。
- 它可能包含多于两个的线程和同步对象(也就是说,死锁在比刚讲述的例子有更多复杂的事件序列的时候可以发生)。
11、信号量
信号量用于限制访问共享资源的线程数
在计算机科学中,信号量是一个对象,它控制着对公共的访问。访问资源之前,线程必须从信号量获得许可,访问结束后,线程必须返还许可给信号量,如下图所示:
为了创建信号量,你必须指定许可数目,另fairness可选, 如下图所示。一个task分别通过调用信号量的acquire()和release() 方法获得许可和释放许可。一旦许可被获取,信号量的可用许可数减 1,被释放则加 1。
许可数仅为1 的信号量可用于模拟互斥锁,下面的例子使用信号量以保证一个时间段内仅有一个线程可访问deposit方法。一个线程在执行deposit方法时首先获得许可,余额更新后,线程释放许可。永远将release() 方法放在finally语句中是一种很好的做法,这种做法可保证即使出现了异常,许可最终可被释放。
12、Thread 方法
下表列出了Thread类的一些重要方法:
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。 |
测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。
序号 | 方法描述 |
---|---|
1 | public static void yield() yieId() 方法的作用是放弃当前的 CPU 资源,将它让给其他的任务去占用 CPU 执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),这个“正在执行的线程”是指 this.currentThread() 返回的线程。此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
join()方法、yield()方法和sleep()方法
join()方法的作用:让“主线程”等待“子线程”结束之后再继续运行。这句话可能有点晦涩,我们还是通过例子去理解:
// 主线程
public class Father extends Thread {
public void run() {
Son s = new Son();
s.start();
s.join();
...
}
}
// 子线程
public class Son extends Thread {
public void run() {
...
}
}
上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。在Father主线程中,通过new Son()新建一个“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。这也就是我们所说的join()的作用,让主线程等待,一直等到子线程结束之后,主线程才能继续运行。。
sleep()、yield()、join()等是Thread类的方法(而wait()和notify()是Object类的方法)。yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么yield()方法将不会起作用。
sleep()使当前线程进入超时等待状态(见上面的状态转移图),所以执行sleep()的线程在指定的时间内肯定不会被执行;sleep()方法只让出了CPU,而并不会释放同步资源锁。
sleep()方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield()方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
另外,sleep()方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep()方法,又没有受到 I\O 阻塞,那么,较低优先级的线程只能等待所有较高优先级的线程运行结束,才有机会运行。
13、多线程实例
13.1、三个售票窗口同时出售20张票
程序分析:
(1)票数要使用同一个静态值
(2)为保证不会出现卖出同一个票数,要java多线程同步锁。
设计思路:
(1)创建一个站台类Station,继承Thread,重写run方法,在run方法里面执行售票操作!售票要使用同步锁:即有一个站台卖这张票时,其他站台要等这张票卖完!
(2)创建主方法调用类
(一)创建一个站台类,继承Thread
package com.ykx.thread;
public class Station extends Thread {
// 通过构造方法给线程名字赋值
public Station(String name) {
super(name);// 给线程名字赋值
}
// 为了保持票数的一致,票数要静态
static int tick = 20;
// 创建一个静态钥匙
static Object ob = "aa";//值是任意的
// 重写run方法,实现买票操作
@Override
public void run() {
while (tick > 0) {
synchronized (ob) {// 这个很重要,必须使用一个锁,
// 进去的人会把钥匙拿在手上,出来后才把钥匙拿让出来
if (tick > 0) {
System.out.println(getName() + "卖出了第" + tick + "张票");
tick--;
} else {
System.out.println("票卖完了");
}
}
try {
sleep(1000);//休息一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(二)创建主方法调用类
package com.ykx.thread;
public class ThredTest {
/**
* java多线程同步锁的使用
* 示例:三个售票窗口同时出售10张票
* */
public static void main(String[] args) {
//实例化站台对象,并为每一个站台取名字
StationThread station1=new StationThread("售票窗口1");
StationThread station2=new StationThread("售票窗口2");
StationThread station3=new StationThread("售票窗口3");
// 让每一个站台对象各自开始工作
station1.start();;
station2.start();
station3.start();
}
}
注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
多执行几次,发现由哪个窗口卖固定的的第几张票是不确定的,从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程 。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
参考博文链接:
https://blog.csdn.net/fuzhongmin05/article/details/71425191
https://blog.csdn.net/qq_34996727/article/details/80416277