基本概念
程序(program)
程序是为完成特定任务,用某种语言编写的一组指令和数据的有序集合,可以理解为一段静态未运行的代码。
进程(process)
进程是程序的一次执行过程,是系统执行资源分配和调度的独立单位,系统运行一个程序即是一个进程从创建、运行到消亡的过程。每一个进程都有属于自己的存储空间和系统资源。比如运行中的QQ、微信、播放器、游戏、IDE等
线程(thread)
线程是进程中的实际运行单位,是独立运行于进程之中的子任务,可以看成进程中的单个顺序控制流。它是cpu任务调度和执行的基本单位。一个进程至少包含一个线程。
并行/并发
- 并行:多个cpu同时执行多个任务。多个cpu实例或者多台机器同时执行多个任务,是真正的同时。
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。
多线程
一个进程下多个线程并发执行。
CPU单核和多核
单核的CPU是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。同时间段内有多个线程需要CPU去运行时,CPU也只能交替去执行多个线程中的一个线程,但是由于其执行速度特别快,因此感觉不出来。
多核的CPU才能更好的发挥多线程的效率。
对于Java应用程序java.exe来讲,至少会存在三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。如过发生异常时会影响主线程。
线程创建方式
继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
//重写run方法 编写业务代码
}
public static void main(String[] args) {
//创建线程
MyThread myThread=new MyThread();
//调用start方法启动线程,等待cpu调度后执行run方法 注意:如果直接调用run方法不是创建新线程 只是普通方法
myThread.start();
}
}
实现Runnable接口
由于java单继承的特点,相比较直接Thread类,实现Runnable接口更通用。
public class MyRunnable implements Runnable {
@Override
public void run() {
}
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable();//创建Runnable实现类对象
Thread thread = new Thread(myRunnable);//Thread类构造传参
thread.start();
}
}
实现Callable接口
与Runnable相比,Callable功能更强大
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
//1.创建一个实现Callable的实现类
public class MyCallable implements Callable<Integer> {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Integer call() throws Exception {
return 1;
}
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
MyCallable myCallable = new MyCallable();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
FutureTask<Integer> futureTask = new FutureTask(myCallable);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中Call方法的返回值
Integer num = futureTask.get();
System.out.println("返回:" + num);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
线程池创建
背景:
经常创建和销毁、使用量特别大的资源、比如并发情况下的线程、对性能影响很大。
思路:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
public class MyThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(5);
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象。
service.execute(new Runnable() {
public void run() {
//业务代码
}
});
//3.关闭线程池
service.shutdown();
}
}
Executors-可以看成是线程池工厂类 通过newFixedThreadPool方法创建线程池 newFixedThreadPool返回的是ExecutorService 它是Executor的子接口
public interface ExecutorService extends Executor {}
线程池任务执行流程如下
线程生命周期
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
- 新建(new):new Thread()创建线程,系统还没为它分配资源
- 就绪(runnable):调用线程的start()方法后,这时候线程处于等待CPU分配资源阶段,对于多线程而言,cpu调度具有一定的随机性,谁先抢到CPU资源,谁先执行。
- 运行(running):当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的业务逻辑
- 阻塞(blocked):在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态
- 销毁(terminated):如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源
线程状态控制
1.start()
启动当前线程, 调用当前线程的run()方法
2.run()
通常需要重写Thread类中的此方法,在此方法块中编写要执行的业务代码
3.yield()
释放当前CPU的执行权。调用该方法后,线程对象进入就绪状态,所以完全有可能:某个线程调用了 yield() 方法,但是线程调度器又把它调度出来重新执行。
4.join()
在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 直到线程b完全执行完以后, 线程a才结束阻塞状态
5.sleep(long militime)
让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态。
6.wait()
一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。
7.sleep()和wait()的异同
相同点:两个方法一旦执行,都可以让线程进入阻塞状态。
不同点:
1) 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
2) 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块中调用。
3) 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep不会释放锁,wait会释放锁。
8.notify()
一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。
9.notifyAll()
一旦执行此方法,就会唤醒所有被wait的线程 。
10.LockSupport
LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。
其它常用方法
- currentThread() : 静态方法, 返回当前代码执行的线程
- getName() : 获取当前线程的名字
- setName() : 设置当前线程的名字
- isAlive() :判断当前线程是否存活
- getPriority():获取线程优先级
- setPriority():设置线程优先级
线程的优先级等级(一共有10挡)
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5 (默认优先级)
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有高优先级的线程执行完成以后,低优先级的线程才执行。
线程同步
多线程操作和访问共享数据时,可能会破坏数据,出现数据的安全性问题。比如售票、取钱等场景。
示例代码
public class TicketTest {
public static void main(String[] args) {
TicketRunnable ticketRunnable = new TicketRunnable();
//开启3个售票窗口同时卖票
Thread t1 = new Thread(ticketRunnable);
Thread t2 = new Thread(ticketRunnable);
Thread t3 = new Thread(ticketRunnable);
t1.start();
t2.start();
t3.start();
}
}
class TicketRunnable implements Runnable {
int ticketCount = 10;//10张票
@Override
public void run() {
while (ticketCount > 0) {
try {
//因为cpu执行速度比较快 所以手动让线程进入阻塞, 提高问题出现概率
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程" + Thread.currentThread().getName() + "-票数剩余:" + --ticketCount);
}
}
}
执行结果
线程Thread-0-票数剩余:9
线程Thread-2-票数剩余:7
线程Thread-1-票数剩余:8
线程Thread-2-票数剩余:6
线程Thread-1-票数剩余:4
线程Thread-0-票数剩余:5
线程Thread-1-票数剩余:3
线程Thread-0-票数剩余:1
线程Thread-2-票数剩余:2
线程Thread-0-票数剩余:0
线程Thread-1-票数剩余:0
线程Thread-2-票数剩余:-1
可以看到,出现了票数为-1,即多售票的情况。这是因为多个线程在及其接近的时间段内都判断通过,而在前一个线程执行完ticketCount-1操作后,ticketCount可能已经等于0,但是剩余线程依然会执行完票数-1的操作。
简单来说,线程的安全性问题主要是因为多个线程正在执行代码的过程中,并且尚未完成的时候,其他线程参与进来执行代码所导致的。正确情况下,我们是希望run方法里面的代码块作为整体判断执行。
在Java中,我们通过同步机制,来解决线程的安全问题。
同步代码块
synchronized(同步监视器){需要被同步的代码}
注意点:
- 尽量只包含必要的代码,因为效率问题。也不能包含在while外面,否则就变成了单个线程循环执行。
- 同步监视器也称锁,可以是任一对象。但是多个线程必须使用同一把锁,可以使用this、类.class或者一个共享对象。
示例
class TicketRunnable implements Runnable {
int ticketCount = 10;//10张票
public void run() {
while (ticketCount > 0) {
synchronized (this){
if(ticketCount>0){//同步代码块里需要再次判断
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "-票数剩余:" + --ticketCount);
}
}
}
}
}
结果
线程Thread-0-票数剩余:9
线程Thread-0-票数剩余:8
线程Thread-0-票数剩余:7
线程Thread-0-票数剩余:6
线程Thread-0-票数剩余:5
线程Thread-2-票数剩余:4
线程Thread-2-票数剩余:3
线程Thread-2-票数剩余:2
线程Thread-2-票数剩余:1
线程Thread-2-票数剩余:0
缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于时一个单线程的过程,效率低。
同步方法
简单理解就是使用synchronized修饰需要同步的方法。
示例
class TicketRunnable implements Runnable {
int ticketCount = 10;//10张票
public void run() {
while (ticketCount > 0) {
sell();
}
}
public synchronized void sell(){
if (ticketCount > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "-票数剩余:" + --ticketCount);
}
}
}
Lock锁
JDK5.0之后,可以通过实例化ReentrantLock对象,在所需要同步的语句前,调用ReentrantLock对象的lock()方法,实现同步锁,在同步语句结束时,调用unlock()方法结束同步锁。
synchronized和lock的异同:
1. Lcok是显式锁(需要手动开启和关闭锁),synchronized是隐式锁,除了作用域自动释放。
2. Lock只有代码块锁,synchronized有代码块锁和方法锁。
3. 使用Lcok锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)
建议使用顺序:Lock—》同步代码块(已经进入了方法体,分配了相应的资源)—》同步方法(在方法体之外)
class TicketRunnable implements Runnable {
int ticketCount = 10;//10张票
ReentrantLock lock = new ReentrantLock();
public void run() {
while (ticketCount > 0) {
try {
lock.lock();
if (ticketCount > 0) {
Thread.sleep(10);
System.out.println("线程" + Thread.currentThread().getName() + "-票数剩余:" + --ticketCount);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
}
结果
线程Thread-0-票数剩余:9
线程Thread-0-票数剩余:8
线程Thread-0-票数剩余:7
线程Thread-0-票数剩余:6
线程Thread-1-票数剩余:5
线程Thread-1-票数剩余:4
线程Thread-1-票数剩余:3
线程Thread-1-票数剩余:2
线程Thread-2-票数剩余:1
线程Thread-0-票数剩余:0
线程死锁
不同线程互相持有对方的锁,都在等待对方先释放自己需要的同步资源,会造成死锁。死锁不会报异常,而是会阻塞程序,所以编写代码时需要避免产生死锁的可能。
示例代码
public class DeadLock {
public static void main(String[] args) {
final Object o1 = new Object();
final Object o2 = new Object();
new Thread(){
@Override
public void run() {
synchronized (o1){
System.out.println("11");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("1111");
}
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (o2){
System.out.println("22");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("2222");
}
}
}
}.start();
}
}
返回结果-程序阻塞:
11
22
线程通信
很多情况下,尽管我们创建了多个线程,也会出现几乎一个线程执行完所有操作的时候,这时候我们就需要让线程间相互交流。
原理:
当一个线程执行完成其所应该执行的代码后,手动让这个线程进入阻塞状态,这样一来,接下来的操作只能由其他线程来操作。当其他线程执行的开始阶段,再手动让已经阻塞的线程停止阻塞,进入就绪状态,虽说这时候阻塞的线程停止了阻塞,但是由于现在正在运行的线程拿着同步锁,所以停止阻塞的线程也无法立马执行。如此操作就可以完成线程间的通信。
所用的到方法:
wait():一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。
notify():一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。
notifyAll() :一旦执行此方法,就会唤醒所有被wait的线程
说明:
这三个方法必须在同步代码块或同步方法中使用。
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
这三个方法并不时定义在Thread类中的,而是定义在Object类当中的。因为所有的对象都可以作为同步监视器,而这三个方法需要由同步监视器调用,所以任何一个类都要满足,那么只能写在Object类中。
sleep()和wait()的异同:(面试题)
-
相同点:两个方法一旦执行,都可以让线程进入阻塞状态。
-
不同点:1) 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
2) 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块中调用。
2) 关于是否释放同步监视器:如果两个方法都使用在同步代码块呵呵同步方法中,sleep不会释放锁,wait会释放锁。
代码示例
class TicketRunnable implements Runnable {
int ticketCount = 10;//10张票
public void run() {
while (ticketCount > 0) {
synchronized (this){
notifyAll();
if(ticketCount>0){
System.out.println("线程" + Thread.currentThread().getName() + "-票数剩余:" + --ticketCount);
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
输出结果
线程Thread-0-票数剩余:9
线程Thread-2-票数剩余:8
线程Thread-1-票数剩余:7
线程Thread-2-票数剩余:6
线程Thread-0-票数剩余:5
线程Thread-1-票数剩余:4
线程Thread-2-票数剩余:3
线程Thread-1-票数剩余:2
线程Thread-0-票数剩余:1
线程Thread-2-票数剩余:0
一个线程执行完后,调用wait方法释放锁,此时会由cpu调度从剩余的线程中选择一个线程执行,以此反复。