java 中的多线程
简介
进程 : 指正在运行的程序,并具有一定的独立能力,即 当硬盘中的程序进入到内存中运行时,就变成了一个进程
线程 : 是进程中的一个执行单元,负责当前程序的执行。线程就是CPU通向程序的路径
一个进程中只有一个线程,单线程程序
一个进程中是可以有多个线程的,这个应用程序是多线程程序
程序的运行分类
分时调度
所有线程轮流使用CPU 的使用权,平均分配每个线程占用CPU 的时间
抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程(线程的随机性)。
java 使用的为抢占式调度
抢占式调度简介:
现在的操作系统都支持多进程并发运行,比如:一边用office ,一边使用QQ,一边看着视频 等等,
看着好像这些程序都在同一时刻运行,实际上是 CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。
对于CPU的一个核而言,某个时刻只能执行一个线程,而CPU 在多个线程之间切换的速度相对我们而言感觉要快,看上去就是在同一时刻运行。
注意:
多线程并不能提高程序的运行速度,但能够提高程序的运行效率,让CPU 的使用效率更高。
多线程的由来
jvm启动后,必然有一个执行线程(路径)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程(main线程)。
若主线程遇到循环,并循环次数较多,则导致程序在指定的位置停留时间过长,无法马上执行下面的程序,则需要等待循环结束后才能够执行代码。效率慢。
主线程负责执行其中的一个循环,由另一个线程执行另一个循环,最终实现多部分代码同时执行。多线程之间互不影响。
多线程的创建方式
1、继承 Thread 类
创建一个类,继承 Thread 类,并重写Thread 类的 run 方法
创建对象,调用 run 方法 ,就相当于执行其他线程的 main 方法。
步骤:
1、自定义一个类,继承Thread 类
2、重写Thread 类中的 run 方法 ,设置线程任务
3、创建自定义类的实例
4、实例化 Thread 类,并传入自定义的类
4、调用start ,开启进程。
示例:
1 1、自定义类 2 // 创建一个类,继承Thread 3 public class Thread_01 extends Thread{ 4 // 重写run 方法,在run方法中定义线程任务 5 public void run(){ 6 System.out.println(Thread.currentThread().getName()); 7 } 8 } 9 2、main方法 10 public class ThreadDemo { 11 public static void main(String[] args) { 12 // 创建线程对象 t1 13 Thread_01 t1 = new Thread_01(); 14 // 为了启动Thread_01 这个线程,需要实例化Thread,并传入自己的Thread_01实例 15 Thread thread = new Thread(t1); 16 // 通知CPU 要启动线程 17 thread.start(); 18 System.out.println(Thread.currentThread().getName()); 19 } 20 }
2、实现 Runnable 接口
创建一个类,实现 Runnable 接口,重写 run 方法
步骤:
1、自定义一个类,实现 Runnable 接口
2、重写run 方法,在run方法中设置线程任务
3、在main 方法中,创建自定义类的实例化
4、实例化Thread 类,并传入自定义类的实例化
5、调用start 方法,开启进程
1 1、自定义类,实现runnable 接口 2 // 自定义类,实现Runnable 接口 3 public class Runnable_01 implements Runnable{ 4 // 重写run 方法,在run方法中设置线程任务 5 @Override 6 public void run() { 7 System.out.println(Thread.currentThread().getName()); 8 } 9 10 } 11 12 2、在main方法中,调用 13 public class ThreadDemo { 14 public static void main(String[] args) { 15 // 创建线程对象 t1 16 Runnable_01 t1 = new Runnable_01(); 17 // 为了启动Runnable_01 这个线程,需要实例化Thread,并传入自己的Runnable_01实例 18 Thread thread = new Thread(t1); 19 // 通知CPU 要启动线程 20 thread.start(); 21 System.out.println(Thread.currentThread().getName()); 22 } 23 }
注意:
调用 start 方法,开启新线程。若没有调用start 方法,只调用run 方法,只是调用了一个方法而已,并没有开启新线程
一个线程只能调用一次start 方法,若线程执行结束后,不能再调用。
常用API
-
void
start() 使该线程开始执行;Java 虚拟机调用该线程的
run
方法。static
Thread
currentThread() 返回对当前正在执行的线程对象的引用string getName() 返回该线程的名称
long getId() 返回该线程的标识符
int getPriority() 返回线程的优先级
Thread.State getState() 返回该线程的状态
void interrupt() 中断线程
void setName(String name) 改变线程名称,使之与参数
name
相同void setPriority(int newPriority)更改线程的优先级
static void
sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)static void
yield() 暂停当前正在执行的线程对象,并执行其他线程
1 public class Demo { 2 public static void main(String[] args) { 3 // 调用currentThread().getName(),获取当前线程的名称 4 System.out.println(Thread.currentThread().getName() + "123"); 5 // 调用currentThread().getId(),获取当前线程的标识符 6 System.out.println(Thread.currentThread().getId()); 7 // 调用currentThread().getPriority(),获取当前线程的优先级 8 System.out.println(Thread.currentThread().getPriority()); 9 // 调用currentThread().setName() 给当前线程设置新名称 10 Thread.currentThread().setName("线程新名称"); 11 // 调用currentThread().getName(),获取当前线程的名称 12 System.out.println(Thread.currentThread().getName() + "123"); 13 /** 14 * 打印结果 :main123 15 * 1 16 * 5 17 * 线程新名称123 18 */ 19 20 } 21 }
线程安全
若多线程调用全局变量时,会出现线程安全问题。
即:使用java 模拟窗口卖票时,一个窗口就是一个线程,若同时卖票,可能会出现几个窗口同时卖一张票,或者卖出不存在的票(就剩一张票时,两个窗口同时卖出)
所以,使用多线程时,要注意线程安全问题,解决线程安全问题有三种方式,
方式一:同步代码块
同步代码块:就是在方法块声明上加上 synchronized
synchronized (锁对象) { 可能会产生线程安全问题的代码 }
注意:
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
对象可以是this, 哪个对象调用,方法中的this就是哪个对象
示例:
1 /* 2 * 开启3个线程,同时卖100张票 3 */ 4 public class Demo01PayTicket { 5 public static void main(String[] args) { 6 //创建接口的实现类对象 7 RunnableImpl r = new RunnableImpl(); 8 //创建线程对象 9 Thread t0 = new Thread(r); 10 Thread t1 = new Thread(r); 11 Thread t2 = new Thread(r); 12 //开启多线程 13 t0.start(); 14 t1.start(); 15 t2.start(); 16 } 17 } 18 19 /* 20 * 发现程序出现了安全问题:卖出了重复的票和不存在的票 21 * 22 * 多线程安全问题的第一种解决方案:使用同步代码块 23 * 24 * 注意: 25 * 代码块中传递的锁对象必须保证唯一,多个线程使用的是同一个锁对象 26 * 锁对象可以是任意的对象 27 * 28 */ 29 public class RunnableImpl implements Runnable{ 30 31 //定义一个共享的票源 32 private int ticket = 100; 33 //创建一个锁对象 34 Object obj = new Object(); 35 36 @Override 37 public void run() { 38 //让卖票重复执行 39 while(true){ 40 //同步代码块 41 synchronized (obj) { 42 //判断是否还有票 43 if(ticket>0){ 44 45 //提高安全问题出现的概率,增加一个sleep 46 try { 47 Thread.sleep(10); 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } 51 52 //进行卖票 53 System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); 54 ticket--; 55 } 56 } 57 } 58 } 59 }
方式二:同步方法
1、同步方法:在方法声明上加上 synchronized
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步方法中的锁对象是 this,哪个对象调用,方法中的this就是哪个对象
2、静态同步方法:在方法声明上加上 static synchronized
public static synchronized void method(){
可能会产生线程安全问题的代码
}
静态同步方法中的锁对象不是对象,是本类的 .class 文件
示例:
1 /* 2 * 开启3个线程,同时卖100张票 3 */ 4 public class Demo01PayTicket { 5 public static void main(String[] args) { 6 //创建接口的实现类对象 7 RunnableImpl r = new RunnableImpl(); 8 //创建线程对象 9 Thread t0 = new Thread(r); 10 Thread t1 = new Thread(r); 11 Thread t2 = new Thread(r); 12 //开启多线程 13 t0.start(); 14 t1.start(); 15 t2.start(); 16 } 17 } 18 19 /* 20 * 发现程序出现了安全问题:卖出了重复的票和不存在的票 21 * 22 * 多线程安全问题的第二种解决方案:使用同步方法 23 * 24 * 实现步骤: 25 * 1.把访问了共享数据的代码提取出来放在一个方法中 26 * 2.在方法上添加一个synchronized修饰符 27 * 28 * 格式: 29 * 修饰符 synchronized 返回值类型 方法名(参数){ 30 * 访问了共享数据的代码; 31 * } 32 * 33 * 把选中的代码提取到方法中快捷键:alt+shift+m 34 * 35 */ 36 public class RunnableImpl implements Runnable{ 37 38 //定义一个共享的票源 39 private static int ticket = 100; 40 41 @Override 42 public void run() { 43 //让卖票重复执行 44 while(true){ 45 payTicketStatic(); 46 } 47 48 } 49 50 /* 51 * 静态的同步方法,锁对象不是this 52 * 静态优先于非静态加载到内存中,this是创建对象之后才有的 53 * 锁对象是本类的class属性(反射-->class文件对象) 54 */ 55 public static synchronized void payTicketStatic() { 56 synchronized (RunnableImpl.class) { 57 //判断是否还有票 58 if(ticket>0){ 59 //提高安全问题出现的概率,增加一个sleep 60 try { 61 Thread.sleep(10); 62 } catch (InterruptedException e) { 63 e.printStackTrace(); 64 } 65 //进行卖票 66 System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); 67 ticket--; 68 } 69 } 70 } 71 72 /* 73 * 定义一个卖票的方法 74 * 使用synchronized修饰 75 * 使用锁对象把方法锁住 76 * 这个锁对象是谁? 77 * 创建的实现类对象new RunnableImpl(); 78 * 也就是this,哪个对象调用的方法,方法中的this就是哪个对象 79 */ 80 public synchronized void payTicket() { 81 //System.out.println(this);//cn.itcast.demo08.RunnableImpl@bcda2d 82 synchronized (this) { 83 //判断是否还有票 84 if(ticket>0){ 85 //提高安全问题出现的概率,增加一个sleep 86 try { 87 Thread.sleep(10); 88 } catch (InterruptedException e) { 89 e.printStackTrace(); 90 } 91 92 //进行卖票 93 System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); 94 ticket--; 95 } 96 } 97 } 98 }
方式三:使用lock 锁
Lock 是接口, ReentrantLock 是Lock 的实现类
API
调用
1、创建ReentrantLock 对象
2、在可能产生安全问题代码前调用 lock() 方法,获得锁
3、调用unlock()方法,解锁
ReentrantLock rl = new ReentrantLock();
//获得锁
rl.LOCK
可能会产生线程安全问题的代码
Rl.unlock
示例:
1 /* 2 * 开启3个线程,同时卖100张票 3 */ 4 public class Demo01PayTicket { 5 public static void main(String[] args) { 6 //创建接口的实现类对象 7 RunnableImpl r = new RunnableImpl(); 8 //创建线程对象 9 Thread t0 = new Thread(r); 10 Thread t1 = new Thread(r); 11 Thread t2 = new Thread(r); 12 //开启多线程 13 t0.start(); 14 t1.start(); 15 t2.start(); 16 } 17 } 18 19 /* 20 * 发现程序出现了安全问题:卖出了重复的票和不存在的票 21 * 22 * 多线程安全问题的第三种解决方案:使用Lock锁 23 * java.util.concurrent.locks.Lock接口 24 * Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。 25 * JDK1.5之后出现的新特性 26 * 27 * 实现步骤: 28 * 1.在成员位置创建一个ReentrantLock对象 29 * 2.在访问了共享数据的代码前,调用lock方法,获取锁对象 30 * 3.在访问了共享数据的代码后,调用unlock方法,释放锁对象 31 * 32 */ 33 public class RunnableImpl implements Runnable{ 34 35 //定义一个共享的票源 36 private int ticket = 100; 37 //1.在成员位置创建一个ReentrantLock对象 38 Lock l = new ReentrantLock(); 39 40 @Override 41 public void run() { 42 //让卖票重复执行 43 while(true){ 44 //2.在访问了共享数据的代码前,调用lock方法,获取锁对象 45 l.lock(); 46 try { 47 //可能会出现安全问题的代码 48 //判断是否还有票 49 if(ticket>0){ 50 //提高安全问题出现的概率,增加一个sleep 51 Thread.sleep(10); 52 //进行卖票 53 System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票"); 54 ticket--; 55 } 56 57 } catch (Exception e) { 58 //异常的处理逻辑 59 System.out.println(e); 60 } finally { 61 //一定会执行的代码,资源释放 62 //3.在访问了共享数据的代码后,调用unlock方法,释放锁对象 63 l.unlock();//无论是否异常,都会释放掉锁对象 64 } 65 } 66 67 } 68 }
多线程线程图
线程的五种状态:
新建状态-->运行状态-->死亡(结束)状态
阻塞状态
冻结状态(休眠/无限等待)
新建 :刚创建出来的线程,即 new Thread();
阻塞 :没有抢到CPU,在等待CPU调用
运行 :调用run 方法,在运行状态
死亡 :方法结束,调用完成
休眠 :调用sleep() 方法,进入休眠状态
sleep 是Thread 的一个函数
sleep 指占用CPU 不工作,其他线程无法进入。即:sleep不会让出系统资源;
无限等待 : 调用wait() 方法,未被唤醒
wait 是object 的一个函数,需要调用notify() 来唤醒
wait 指不占用CPU 不工作,其他线程可以进入。即:wait是进入线程等待池中等待,让出系统资源。