基础概念
📖 问题一 : 什么是线程?线程和程序、进程有什么区别?
程序:为实现某种功能,使用计算机语言编写的一系列指令的集合。
指的是静态的代码(例如安装在电脑上的那些文件)
进程:是运行中的程序(如运行中的王者荣耀)进程是操作系统进行资源分配的最小单位。
线程:进程可以进一步细化为线程,是进程中一个最小的执行单元,是cpu进行调度的最小单元
例如:QQ中的一个聊天窗口
进程和线程的关系:
• 一个进程中可以包含多个线程. (一个QQ程序可以有多个聊天窗口)
• 一个线程只能隶属于一个进程. (QQ的聊天窗口只能属于QQ进程)
• 每一个进程至少包含一个线程,也就是我们的主线程(像java中的main方法就是来启动主线程的)在主线程中可以创建并启动其他线程.
• 一个进程的线程共享该进程的内存资源.
📖 问题二 : 什么是多线程?多线程有哪些优缺点?
✎. 顾名思义多线程指:在一个程序中可以创建多个线程执行.
【优点】 提高程序执行效率(多个任务可以在不同的线程中同时执行)
提高了cpu的利用率
改善程序结构,将复杂任务拆分成若干个小任务
【缺点】 线程也是程序,线程越多占用内存也越多,cpu开销变大(可扩充内存或升级cpu)
✰ 线程之间同时对共享资源的访问会相互影响,若不加以控制会导致数据出错.
📖 问题三: 如何解决多线程操作共享数据的问题?
✎. 多个线程同时访问操作同一个共享的数据( 例如买票、抢购等 )时,可能会引起冲突,所以引入
线程 “同步” 机制,即各线程间要有先来后到。
即通过 【 排队+锁 】 在关键的步骤处,使多个线程只能一个一个的执行.
那么问题又来了,什么是锁呢?
锁机制(Lock)
📖 问题四: 什么是锁?锁有什么用?锁怎么用?
✎. 关于synchronized ( 同步锁 )
语法结构:
synchronized(同步锁对象) {
同步代码块
} 1
同步锁对象作用:
用来记录有没有线程进入到同步代码块,如果有线程进入同步代码块,那么其他线程就不能进入同步代码块,直到上一个线程执行完同步代码块的内容,释放锁之后,其他线程才能进入。
同步锁对象要求: 同步锁对象必须是唯一 的。
✎. synchronized还可修饰方法.
synchronized修饰方法时,同步锁对象不需要我们指定,同步锁对象会默认提供:
- 非静态方法 ------ 默认是this
- 静态方法 ------ 锁对象是当前类的class对象 (一个类的对象只有一个)
✎. 关于Lock锁
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,可以显式加锁释放锁.
✎. ReentrantLock与synchronized区别?
- synchronized是一个关键字 ,控制依靠底层编译后的指令去实现.
- synchronized可以修饰一个方法 或一个代码块.
- synchronized是 隐式 的加锁和释放锁,一旦方法或代码块出现异常,会自动释放锁.
- ReentrantLock是一个类 ,依靠java底层代码去控制 (底层有一个同步队列)
- ReentrantLock只能修饰 代码块.
- ReentrantLock需要 手动 的加锁和释放锁, 所以释放锁最好写在finally中 , 一旦出现异常, 保证锁能释放.
误区:不是只要有线程就需要加锁,只有多个线程对同一资源共享时才加锁
创建线程的方式
📖 问题五: 如何创建线程?有几种方式?
创建线程的方式通常有三种:
• 通过继承Thread来创建线程
• 通过实现Runnable接口来创建线程
• 通过实现Callable接口来创建线程
📌 通过继承Thread来创建线程
• 写一个类继承 java.lang.Thread
• 重写run( )
• 线程中要执行的任务都要写在run( )中,或在run( )中进行调用.
public class MyThread extends Thread{//继承Thread类 @Override public void run() {//重写run方法 for (int i = 1; i <= 200; i++) { System.out.println("run"+i); } } }
public static void main(String[] args) { //创建线程 MyThread mythread = new MyThread(); //启动线程 mythread.start(); for (int i = 1; i <= 200; i++) { System.out.println("main"+i); } }
注意:
启动线程调用的是start() ; 不是run()
run()这不是启动线程,只是一个方法调用,没有启动线程,还是单线程模式的。
📖 Thread类中的方法:
run() 用来定义线程要执行的任务代码.
start() 启动线程
currentThread() 获取到当前线程(.得到具体信息)
setName() 为线程设置名字
getState() 获取状态
getPriority() setPriority 获取/设置 优先级
sleep() 让当前线程休眠指定时间.
join() 等待当前线程执行完毕,其他线程再执行.
yield() 主动礼让,退出cpu重新回到等待序列.
📖 关于优先级:
【java中默认优先级为5, 设置优先级范围为1~10】 ( 作用:为操作系统调度算法提供的 )
📌 通过实现Runnable接口来创建线程
• 创建一个类,实现Runnable接口(即只先创建线程要执行的任务)
• 重写任务执行的Run()
• 创建线程,并为线程指定执行任务.
public class MyThread implements Runnable {//实现Runnable接口 @Override public void run() {//重写run方法 for (int i = 0; i < 200; i++) { System.out.println("自定义线程"); } } }
public static void main(String[] args) { //创建任务 MyThread mythread = new MyThread(); //创建线程,并指定执行任务 Thread thread = new Thread(mythread); thread.start(); }
📖 实现Runnable接口创建的优点:
• 因为java是单继承,一旦继承一个类就不能在继承其他类,避免单继承的局限。
• 适合多线程来处理同一份资源时使用
📌 通过实现Callable接口来创建线程
• 相比run( )方法,可以有返回值.
• 方法可以抛出异常.
• 支持泛型的返回值.
• 需要借助FutureTask类,获取返回结果
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class CallableDemo<T> implements Callable<T> { @Override public T call() throws Exception {//可以有返回值,也可以抛出异常 Integer n = 0; for (int i = 0; i < 10; i++) { n += i; } return (T) n; } public static void main(String[] args) throws ExecutionException, InterruptedException { CallableDemo<Integer> callableDemo = new CallableDemo(); FutureTask<Integer> futureTask = new FutureTask<Integer>(callableDemo); Thread thread = new Thread(futureTask); thread.start(); System.out.println(futureTask.get()); } }
这段代码是打印1~10的和,通过这个案例,相信你对实现Callable接口来创建线程已经大致了解
核心代码:
CallableDemo<Integer> callableDemo = new CallableDemo();
FutureTask<Integer> futureTask = new FutureTask<Integer>(callableDemo);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
线程生命周期
📖 问题六: 一个线程的生命周期是怎样的?
线程状态:
新建:刚刚创建了一个线程对象,并没有启动.
就绪:调用start() 后线程就进入到了就绪状态(可运行状态),进入到了操作系统的调度队列.
运行状态:获得了cpu执行权,进入到cpu执行.
阻塞状态:例如调用sleep() ,有线程调用了join(),线程中进行Scanner输入...
死亡/销毁:run()方法中的任务执行完毕了.
状态关系图:
模拟卖票案例: 两个窗口分别售票,票数为10张
public class MyThread extends Thread{//我们使用了继承Thread的方法 static int num =10; //票总数10,且为共享资源,要用static修饰 static String obj = new String();//可以是任意类对象,但必须唯一。 /* synchronized(同步锁对象) { 同步代码块 } */ @Override public void run() {//线程要执行的代码块要写在run()中 while (true){ synchronized (obj){//加锁,一次只能执行一个线程 if(num>0){ try { Thread.sleep(800);//此处加入休眠为了让运行结果更明显,也可不加 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票"); num--; //每抢一张票,总数num(10)就减1 }else{ break; } } } } }
在Main方法中创建线程并启动:
public static void main(String[] args) { //创建两个线程,分别对应两个窗口 MyThread myThread1 = new MyThread(); myThread1.setName("窗口1");//线程1 myThread1.start(); MyThread myThread2 = new MyThread(); myThread2.setName("窗口2");//线程2 myThread2.start(); }
运行结果:
线程通信
📖 问题七: 什么是线程通信?怎么实现线程交替运行?
✎. 线程通信指:多个线程相互调度, 相互牵制,即线程间的相互作用.
wait( ) --- 让线程等待同时释放锁.
notify( ) --- 唤醒等待线程,必须写在同步代码块中进行,必须通过锁对象调用。
notifyAll( )--唤醒所有等待的线程.
这三个方法必须使用在同步代码块或同步方法中
✎. sleep( long time )与wait( )区别:
sleep ( ) :
- 属于Thread类中的方法
- sleep休眠指定时间后,会自动唤醒
- sleep( ) 不会释放锁
wait ( ) :
- 属于Object类中的方法,必须要有锁对象调用
- wait后的线程必须要等待其他线程唤醒(notify或notifyAll)
- wait( ) 自动释放锁
让我们通过一个例题来体会下线程通信吧!
两个线程交替打印1-100之间的数字
public class MyThread extends Thread{ static int num = 1; static String string =new String(); @Override public void run() { while (num<=100){ synchronized (string){ string.notify(); System.out.println(currentThread().getName()+":"+num); num++; try { string.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { MyThread mythread1 = new MyThread(); mythread1.start(); MyThread mythread2 = new MyThread(); mythread2.start(); } }