提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、基本概念?
- 程序:是为了完成特定的任务,而用某种语言编写的一组指令的集合,即指一段静态的代码,静态的对象。
- 进程:是指运行中的程序,比如我们使用QQ时,就启动了一个进程,操作系统就会为该进程分配内存空间,当我们使用微信时,又启动了一个进程,操作系统将再次为其分配内存空间。是程序的一次执行过程,或是正在运行的一个程序,是动态过程,有他自身的产生,存在和消亡过程。
- 线程:
1.线程由进程创建,是线程的一个实体
2.一个进程可以拥有多个线程
3.单线程,同一个时刻,只允许执行一个线程
4.多线程,同一时刻,可以执行多个线程,比如一个微信程序,可以打开多个聊天窗口。
5.并发:同一时刻,多个任务交替执行,造成一种貌似同时的错觉,简单的说,单核cpu实现的多任务就是并发(即:一个cpu执行多个任务)。
6.并行:同一时刻,多个任务同时执行,多核cpu可以实现并行,或者并发和并行(即多个cpu可以同时执行不同的任务)。
单线程和多线程的示意图:
线程的三大优势:
- 系统开销小:
创建和撤销线程的系统开销,以及多个线程之间的切换,都比使用进程进行相同操作要小的多 - 方便通信和资源共享
如果实在进程之间通信,往往要求系统内核的参与,以提供通信机制和保护机制,而线程间通信在同一进程的地址空间内,共享内存和文件,操作简单,无需内核参与。 - 简化程序结构
用户在实现多任务的程序是,采用多线程机制,可以使程序结构清晰,独立性强。
二、线程的调度
时间片:线程的调度采用时间片轮转的方式。
抢占式:高优先级的线程抢占CPU。
线程调度是抢占式调度,即如果在当前线程执行过程中一个更高优先级的线程进入就绪状态,则这个线程立即被调度执行。
注意:对于同优先级的线程组成先进先出的队列,使用时间片策略,对于高优先级的线程,使用抢占式的策略。
三个常量:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
- 通过setPriority(int pri)方法设置优先级 ,getPriority()返回当前线程的优先级。
三、线程的生命周期
五种状态:
- 新建状态:创建一个新的子线程。
- 就绪状态:线程已经句被运行的条件,等待调度程序分配CPU资源使其运行。
- 运行状态:调度程序分配CPU资源给该线程。
- 阻塞状态:线程正等待除了CPU资源意外的某个条件符合或某个事件发生。
- 死亡状态:表示线程已经操作结束。
示例图:
四、创建线程的两种方式
1.继承Thread类
public class MyThread01 extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
//启动线程调用线程的start()方法,注意不能直接调用run方法
MyThread01 myThread01 = new MyThread01();
myThread01.start();
}
}
2.实现Runnable接口
public class MyThread02 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
MyThread02 myThread02 = new MyThread02();
//通过new Thread类的对象,将实现了Runnable接口的子类当做参数传递进去
Thread thread = new Thread(myThread02);
//调用start方法开启线程
thread.start();
}
}
注意:当调用线程的start方法时,run方法会自动被调用,所以说调用start方法就是开启线程。
五、常用方法
- Thread.currentThread():获取当前线程对象,可以调用getName(),获取当前线程的名。
- start():启动当前线程,虚拟机负责调用线程中的run()方法
- sleep(long millis):线程休眠指定的毫秒数,使线程进入阻塞状态,当休眠时间过后,会重新进入就绪状态,等待cpu的调度
- yield():使当前线程放弃占用CPU资源,回到就绪状态,使其它优先级不低于自己的线程有机会被执行,注意:该方法不一定会礼让成功
- join():只有当前线程执行完,才会执行另一个线程
- interrupt():中断线程的阻塞状态(而非中断线程),例如一个线程 sleep(1000000000) ,为了中断这个过长的阻塞过程,可以调用该线程的 interrupt() 方法,中断阻塞。需要注意的是,此时 sleep() 方法会抛出 InterruptedException 异常。
- void isAlive():判定该线程是否处于活动状态,处于就绪、运行和阻塞状态的都属于活动状态。
- void setPriority(int newPriority):设置当前线程的优先级。
- int getPriority():获得当前线程的优先级。
注意::yield() 和 sleep() 的区别
- yield() 方法和 sleep() 方法都是 Thread 类的静态方法,都会使当前处于运行状态的线程放弃 CPU 资源,把运行机会让给别的线程。
- sleep() 方法会给其他线程运行的机会,不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;而 yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。
- 当线程执行了 sleep(long millis) 方法后,将转到阻塞状态,参数 millis 指定了睡眠时间,过了指定时间后会到就绪状态;而当线程执行了 yield() 方法后,将直接转到就绪状态。
六、守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
- 守护线程:一般视为工作线程服务的,当所有的用户线程结束,守护线程自动结束
- 常见的守护线程:垃圾回收机制
示例代码:
package 线程;
public class Test5 {
public static void main(String[] args) {
MyDaemon myDaemon = new MyDaemon();
//将该线程设置为守护线程
myDaemon.setDaemon(true);//子线程中的run方法是死循环,将其设置为守护线程后,当主线程中的代码执行完毕之后,子线程也就执行完毕
myDaemon.start();
//主线程
for(int i = 0;i < 5;i++){
System.out.println("主线程开始调用!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程结束");
}
}
class MyDaemon extends Thread{
@Override
public void run() {
while(true){
System.out.println("子线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
一个程序只有处于守护线程时该程序结束运行,所以即便程序中创建并启动了一个线程 t 且 t 的 run() 方法永久循环输出,仍会在主程序执行完毕后退出程序。
七、多线程数据的共享
当一个数据被多个线程存取的时候,通过检查这个数据的值来进行判断并执行操作是极不安全的。
因为在判断之后,有可能因为 CPU 时间切换或阻塞而挂起,挂起过程中这个数据的值很可能被其他线程修改了,判断条件也可能已经不成立了,但此时已经经过了判断,之后的操作还需要继续进行。这就会造成逻辑的混乱,导致数据不一致。
案例:卖票的问题
package com.lanqiao;
public class SaleTicket {
public static void main(String[] args) {
//创建两个窗口
SaleWindow w1 = new SaleWindow();
SaleWindow w2 = new SaleWindow();
w1.setName("窗口1");
w2.setName("窗口2");
w1.start();;
w2.start();
}
}
class SaleWindow extends Thread{
static int tickets = 100;
@Override
public void run() {
while(tickets > 0){
System.out.println(Thread.currentThread().getName() + ":票数" + (--tickets));
}
}
}
出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也来操作车票
八、多线程同步处理方式一
- 线程同步机制:
- 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性
- 也可以这么理解:线程同步,即当有一个线程在堆内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
- 同步代码块:
示例代码:
实现Runnable接口
package Test;
public class Test5 {
public static void main(String[] args) {
Sell sell = new Sell();
new Thread(sell).start();
new Thread(sell).start();
new Thread(sell).start();
}
}
class Sell implements Runnable{
private int ticket = 100;
boolean flag = true;
//public synchronized void s(){//当前锁对象为this,因为是操作的同一个对象
public void s(){
synchronized (this){ //同步代码块的方式,指的依然是当前的对象,而且是同一个对象
if(ticket <= 0)
{
flag = false;
return;
}
try {
System.out.println(Thread.currentThread().getName() + "剩余票数:" + (--ticket));
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
while(flag){
s();
}
}
}
继承Thread类
package Test;
public class Test5 {
public static void main(String[] args) {
Sell sell = new Sell();
Sell sell1 = new Sell();
Sell sell2 = new Sell();
sell.start();
sell1.start();
sell2.start();
}
}
class Sell extends Thread{
private static int ticket = 100;
private static final Object obj = new Object();
boolean flag = true;
public void s(){
synchronized (obj){ //当前对象时Object类型的对象,三个线程共用一把同步锁
if(ticket <= 0)
{
flag = false;
return;
}
try {
System.out.println(Thread.currentThread().getName() + "剩余票数:" + (--ticket));
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
while(flag){
s();
}
}
}
互斥锁
- java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
- 每个对象都对应于一个可称为”互斥锁“的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象
- 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰符时,表名该对象在任意时刻只能有一个线程访问
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)锁,可以是this,也可以是其他对象例如:Object对象(要求是同一个对象)
- 同步方法(静态的)锁为当前类本身
注意:
- 同步方法如果没有使用static修饰:默认对象为this
- 如果方法使用static修饰,默认对象:当前类.class
- 实现的落地步骤:
- 需要先分析上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个即可
死锁
如果线程 A 只有等待线程 B 的完成才能继续,而在线程 B 中又要等待线程 A 的资源,那么这两个线程相互等待对方释放锁时就会发生死锁。出现死锁后,不会出现异常,因此不会有任何提示,只是相关线程都处于阻塞状态,无法继续运行。
- 死锁产生的原因有以下三个方面:
系统资源不足。如果系统的资源充足,所有线程的资源请求都能够得到满足,自然就不会发生死锁。
线程运行推进的顺序不合适。
资源分配不当等。
产生死锁的必要条件有以下四个:
互斥条件:一个资源每次只能被一个线程使用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
模拟死锁问题
package 线程;
public class DeadLock {
public static void main(String[] args) {
Dead dead = new Dead(true);
Dead dead1 = new Dead(false);
Thread thread = new Thread(dead);
Thread thread1 = new Thread(dead1);
thread.start();
thread1.start();
}
}
class Dead implements Runnable{
private boolean flag;
private static Object o1 = new Object();
private static Object o2 = new Object();
public Dead(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//同一时刻:
//如果flag为真,线程A就会先抢o1对象锁,然后去尝试获取o2对象锁,如果线程A得不到O2对象锁,就会Blocked
//如果flag维嘉,线程B就会先得到o2对象锁,然后去尝试获取o1对象锁,如果线程B得不到O1对象锁,就会Blocked
//此时就形成了死锁的状态
if(flag){
synchronized (o1){
System.out.println("进入1");
synchronized (o2){
System.out.println("进入2");
}
}
}else{
synchronized (o2){
System.out.println("进入3");
synchronized (o1){
System.out.println("进入4");
}
}
}
}
}
释放锁
-
以下操作会释放锁
- 当前线程的同步方法,同步代码块执行结束
- 当前线程在同步代码块,同步方法中遇到break,return
- 当前线程在同步代码块,同步方法中出现了未处理的Error或Exception,导致异常结束
- 当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
-
以下操作不会释放锁
-
线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法暂停当前线程的执行,不会释放锁
-
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁
注意:应尽量避免使用suspend()和resume()来控制线程,方法不在推荐使用
-