多线程的基本概念
程序、进程、线程
程序(program) 是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象
进程(process) 是程序的一次执行过程,或是正在运行的一个程序。是一个动态 的过程:有它自身的产生、存在和消亡的过程。——生命周期
线程(thread) ,进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以 访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
并行与并发
并行 :多个CPU同时执行多个任务。
并发: 一个CPU(采用时间片)同时执行多个任务。
多线程的创建和启动
Thread类
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。
- 线程是程序中执行的线程。 Java虚拟机允许应用程序同时执行多个执行线程。
- 每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。 每个线程可能也可能不会被标记为守护程序。 当在某个线程中运行的代码创建一个新的
Thread
对象时,新线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时才是守护线程。
Thread类的特性
- 每个线程都是通过某个特定Thread对象的
run()
方法来完成操作的,经常把run()
方法的主体称为线程体 - 通过该Thread对象的
start()
方法来启动这个线程,而非直接调用run()
Thread类常用构造方法
Thread() | 分配一个新的 Thread 对象。 |
Thread(String name) | 创建线程并指定线程实例名 |
Thread(Runnable target) | 指定创建线程的目标对象,它实现了Runnable接 口中的run方法 |
Thread(Runnable target, String name) | 创建新的Thread对象 |
Thread类常用方法
static Thread |
currentThread() |
返回对当前正在执行的线程对象的引用。 |
long |
getId() |
返回此线程的标识符。 |
String |
getName() |
返回此线程的名称。 |
void |
join() |
等待这个线程死亡。 |
void |
run() |
如果这个线程使用单独的Runnable 运行对象构造,则调用该Runnable 对象的run 方法; 否则,此方法不执行任何操作并返回。 |
void |
setName(String name) |
将此线程的名称更改为等于参数 name 。 |
void |
setPriority(int newPriority) |
更改此线程的优先级。 |
static void |
sleep(long millis) |
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 |
void |
start() |
导致此线程开始执行; Java虚拟机调用此线程的run 方法。 |
String |
toString() |
返回此线程的字符串表示,包括线程的名称,优先级和线程组。 |
static void |
yield() |
对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。 |
int |
getPriority() |
返回此线程的优先级。 |
Thread.State |
getState() |
返回此线程的状态。 |
boolean |
isAlive() |
测试这个线程是否活着。 |
多线程的4种创建方式
方式一:继承Thread类
步骤:
- 定义子类继承Thread类。
- 子类中重写Thread类中的
run()
方法。 - 创建Thread子类对象,即创建了线程对象。
- 调用线程对象
start()
方法:启动线程,调用run()
方法。
注:
- 如果自己手动调用
run()
方法,那么就只是普通方法,没有启动多线程模式。 run()
方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定。- 想要启动多线程,必须调用
start()
方法。 - 一个线程对象只能调用一次
start()
方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”。
示例
创建并启动一个线程,输出0-99
publicclassMyThreadTest { publicstaticvoidmain(String[] args) { //1.创建线程MyThreadmyThread=newMyThread(); //2.启动线程,并调用当前线程的run()方法myThread.start(); } } classMyThreadextendsThread{ publicMyThread() { } //3.重写run()方法publicvoidrun() { for(inti=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+i); } } }
方式二:实现Runnable接口
void |
run() |
当实现接口的对象 Runnable 被用来创建一个线程,启动线程使对象的 run 在独立执行的线程中调用的方法。 |
步骤:
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的
run()
方法。 - 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的
start()
方法:开启线程,调用Runnable子类接口的run()
方法。
注:
实现Runnable接口,需要通过Thread类的对象调用
start()
示例
创建两个分线程,让其中一个线程输出1-100之间的偶数,另一 个线程输出1-100之间的奇数
publicclassMyThreadTest { publicstaticvoidmain(String[] args) { //1.创建线程MyThreadonemyThreadone=newMyThreadone(); MyThreadTwomyThreadTwo=newMyThreadTwo(); //2.启动线程,并调用当前线程的run()方法myThreadone.start(); //4.通过Thread类的对象调用start()newThread(myThreadTwo).start(); } } classMyThreadoneextendsThread{ publicMyThreadone() { } //3.重写run()方法publicvoidrun() { //输出1-100之间的偶数for(inti=2;i<=100;i+=2){ System.out.println(Thread.currentThread().getName()+":"+i); } } } classMyThreadTwoimplementsRunnable{ //3.重写run()方法publicvoidrun() { //输出1-100之间的奇数for(inti=1;i<100;i+=2){ System.out.println(Thread.currentThread().getName()+":"+i); } } }
方式三:实现Callable接口
从Java1.5
版本开始,就提供了 Callable
和 Future
来创建线程
Thread
和Runnable
两种方式创建线程,不过这两种方式创建线程都有一个缺陷:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果。
而如果使用Callable
和Future
,通过它们就可以在任务执行完毕之后得到任务执行结果。
Callable
产生结果,Future
获取结果。
步骤:
- 创建 Callable 接口的实现类,并实现
call()
方法,该call()
方法将作为线程执行体,并且有返回值; - 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的
call()
方法的返回值; - 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程;
- 调用 FutureTask 对象的
get()
方法来获得子线程执行结束后的返回值。
注:
在实现Callable
接口中,此时不再是run()
方法了,而是call()
方法,此call()
方法作为线程执行体,同时还具有返回值!
通过FutureTask
拿到的,使用.get()
方法即可获得线程的返回值,
Callable类
V |
call() |
FutureTask类
构造方法:
FutureTask(Callable<v> allable) |
创建一个 FutureTask ,它将在运行时执行给定的 Callable 。 |
FutureTask([Runnable runnable, V result) |
创建一个 FutureTask ,将在运行时执行给定的 Runnable ,并安排 get 将在成功完成后返回给定的结果。 |
常用方法:
V |
get() |
等待计算完成,然后检索其结果。 |
示例
在runThread(int num)
函数中执行线程,创建Callable
线程,Callable
线程需要执行求第num
项斐波那契数列的值,最后在runThread
函数中获取Callable
线程执行的结果,并打印输出。
publicclassMyThreadTest { publicstaticvoidmain(String[] args) throwsExecutionException, InterruptedException { Scannerscanner=newScanner(System.in); intnum=scanner.nextInt(); //1.创建 Callable 实现类的实例ThreadCallablethreadCallable=newThreadCallable(num); //2.使用 FutureTask 类来包装 Callable 对象FutureTaskfutureTask=newFutureTask(threadCallable); //4.使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程newThread(futureTask).start(); //5.调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值System.out.println(futureTask.get()); } } classThreadCallableimplementsCallable { intnum; //带参构造方法publicThreadCallable(intnum) { super(); this.num=num; } //3.重写call()方法publicIntegercall() throwsException { returnfun(num); } //输出斐波那契数列publicintfun(intnum) { if (num<3) { return1; } elsereturnfun(num-1) +fun(num-2); } }
方式四:使用线程池
提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
JDK 5.0起提供了线程池相关API:ExecutorService
和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
ExecutorService线程池接口类
void |
shutdown() |
启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。 |
<T> Future<T> |
submit(Callable<T> task) |
提交值返回任务以执行,并返回代表任务待处理结果的Future。 |
Future<?> |
submit(Runnable task) |
提交一个可运行的任务执行,并返回一个表示该任务的未来。 |
Executors工具类
static Callable<Object> |
callable(Runnable task) |
执行任务/命令,没有返回值,一般用来执行 Runnable |
static ExecutorService |
newCachedThreadPool() |
创建一个根据需要创建新线程的线程池 |
static ExecutorService |
newFixedThreadPool(int nThreads) |
创建一个可重用固定线程数的线程池 |
ExecutorService |
newSingleThreadExecutor() |
创建一个只有一个线程的线程池 |
static ScheduledExecutorService |
newScheduledThreadPool(int corePoolSize) |
创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行 |
示例
用线程池创建线程,一个输出奇数,一个输出偶数
publicclassMyTest { publicstaticvoidmain(String[] args) { //1.提供指定线程数量的线程池ExecutorServicees=Executors.newFixedThreadPool(10); ThreadPoolExecutorthreadpool= (ThreadPoolExecutor) es; //2.执行指定线程池的操作,需要提供实现Runnable接口和Callable接口实现类的对象//提供实现Runnable接口实现类的对象threadpool.execute(newRunnableThread()); //提供实现Callable接口实现类的对象threadpool.submit(newCallableThread()); //3.关闭线程池threadpool.shutdown(); } } classRunnableThreadimplementsRunnable{ publicvoidrun() { for(inti=1;i<=100;i++) { if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i); } } } classCallableThreadimplementsCallable{ publicObjectcall() throwsException { for(inti=1;i<=100;i++) { if(i%2==1) System.out.println(Thread.currentThread().getName()+":"+i); } returnnull; } }
线程的状态与调度
线程的状态与调度
如果看懂下图,你对线程的了解就会更上一层楼。
- 新建状态(
New
):当我们使用new
关键字新建一个线程 - 就绪状态(
Runnable
);调用start
方法启动线程,这个时候就进入了可运行状态 - 运行状态(
Running
):就绪状态获取了CPU资源,开始执行run
方法; - 阻塞状态(
Blocked
):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种;
- 等待阻塞:运行的线程执行
wait()
方法,JVM会把该线程放入等待池中。(wait
会释放持有的锁); - 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;
- 其他阻塞:运行的线程执行
sleep()
或join()
方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()
状态超时、join()
等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep
是不会释放持有的锁);
- 死亡状态(
Dead
):线程执行完了或者因异常退出了run()
方法,该线程结束生命周期。
线程执行的优先级
在Java
中线程有优先级,优先级高的线程会获得较多的运行机会。
Java
线程的优先级用整数表示,取值范围是1~10
,Thread
类有以下三个静态常量:
staticintMAX_PRIORITY线程可以具有的最高优先级,取值为10。staticintMIN_PRIORITY线程可以具有的最低优先级,取值为1。staticintNORM_PRIORITY分配给线程的默认优先级,取值为5。
如果要设置和获取线程的优先级,可以使用Thread
类的setPriority()
和getPriority()
方法。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY
。
线程的优先级有继承关系,比如A
线程中创建了B
线程,那么B
将和A
具有相同的优先级。
JVM提供了10
个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread
类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
关于线程调度与优先级你还需要了解:
- 线程睡眠:
Thread.sleep(long millis)
方法,使线程转到阻塞状态。millis
参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable
)状态。sleep()
平台移植性好。 - 线程等待:
Object
类中的wait()
方法,导致当前的线程等待,直到其他线程调用此对象的notify()
方法或notifyAll()
唤醒方法。这个两个唤醒方法也是Object
类中的方法,行为等价于调用wait(0)
一样。 - 线程让步:
Thread.yield()
方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。 - 线程加入:
join()
方法,等待其他线程终止。在当前线程中调用另一个线程的join()
方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。 - 线程唤醒:
Object
类中的notify()
方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个wait
方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll()
,唤醒在此对象监视器上等待的所有线程。
线程常用函数
sleep()函数
sleep(long millis)
: 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
使用方式很简单在线程的内部使用Thread.sleep(millis)
即可。
sleep()
使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用,目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
在sleep()
休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
join()函数
join()
函数的定义是指:等待线程终止。
yield() 函数
yield
函数可以理解为“让步”,它的作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()
应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的
目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()
达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
wait() 函数和notify()函数
从功能上来说:
wait()
就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()
唤醒该线程,才能继续获取对象锁,并继续执行;- 相应的
notify()
就是对对象锁的唤醒操作。
但有一点需要注意的是notify()
调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}
语句块执行结束,自动释放锁后,JVM会在wait()
对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。
Thread.sleep()
与Object.wait()
二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()
在释放CPU同时,释放了对象锁的控制。
Volatile关键字
- 当一个共享变量被volatile修饰时,它就具备了“可见性”,即这个变量被一个线程修改时,这个改变会立即被其他线程知道。
- 当一个共享变量被volatile修饰时,会禁止“指令重排序”。
线程同步
并发编程什么时候会出现安全问题
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。这个就是线程安全问题,多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。
synchronized关键字
互斥锁,互斥锁:就是能达到互斥访问目的的锁。
如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。
在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。
使用synchronized
修饰对象的方法或者代码块,当某个线程访问这个对象synchronized
方法或者代码块时,就获取到了这个对象的锁,
这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行该对象的方法。
在使用synchronized
关键字的时候有几个问题需要我们注意:
在线程调用synchronized的方法时,其他synchronized的方法是不能被访问的,道理很简单,一个对象只有一把锁; 当一个线程在访问对象的synchronized方法时,其他线程可以访问该对象的非synchronized方法,因为访问非synchronized不需要获取锁,是可以随意访问的; 如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
synchronized代码块
synchronized(synObject) { //共享代码块}
当在某个线程中执行该段代码时,该线程会获取到该对象的synObject
锁,此时其他线程无法访问这段代码块,synchronized
的值可以是this
代表当前对象,也可以是对象的属性,用对象的属性时,表示的是对象属性的锁。
synchronized的锁是什么?
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块
:自己指定,很多时候也是指定为this或类名.class
注意:
- 必须确保使用同一个资源的
多个线程共用一把锁
,这个非常重要,否则就 无法保证共享资源的安全 - 一个线程类中的所有静态方法共用同一把锁(
类名.class
),所有非静态方 法共用同一把锁(this
),同步代码块(指定需谨慎)
Lock(锁)
classA { privatefinalReentrantLocklock=newReenTrantLock(); publicvoidm() { lock.lock(); try { //保证线程安全的代码; } finally { lock.unlock(); } } }
通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock
类实现了 Lock
,它拥有与 synchronized
相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以 显式加锁、释放锁。
synchronized 与 Lock 的对比
Lock
是显式锁(手动开启和关闭锁,别忘记关闭锁
),synchronized
是 隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)
优先使用顺序:
Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法 (在方法体之外)