多线程的创建使用和Thread类
- 一、多线程相关概念
- 1. 并行与并发
- 2. 进程与线程
- 3. 多线程的作用
- 4. 线程调度
- 二、多线程创建使用
- 1.经典的两种方式
- 2. 匿名内部类实现
- 3 Thread类
- 3.1 构造器
- 3.2 基本方法
- 3.3 线程控制方法
- 3.4 守护线程
- 三、 线程的生命周期
- 四、线程安全
- 方式1:继承Thread类
- 方式2:实现Runnable接口
- 出现线程安全问题的前提条件
- 解决方法:同步代码块synchronized
- 锁对象
- 编写多线程程序的一般原则
- 单例模式之懒汉式的同步代码块
一、多线程相关概念
1. 并行与并发
- 并行:同一个时间点,多个事件同时执行.
- 并发:同一个微小的时间段,多个事件正在执行.
2. 进程与线程
- 进程:是程序运行过程的描述,系统以进程为单位为每个程序分配独立的系统资源,比如内存。
- 线程:线程是进程中的执行单元,主要负责执行程序的任务,每个进程至少有一个线程。
3. 多线程的作用
主要优点:充分利用CPU空闲时间片,用尽可能短的时间完成用户的请求,即使程序的响应速度更快。
4. 线程调度
表示CPU计算资源在不同线程中切换。有不同的调度方式:
- 分时调度:所有线程轮流使用CPU使用权,平均分配。
- 抢占式调度:按照优先级抢占,同级的话随机分配。
二、多线程创建使用
1.经典的两种方式
实现多线程有两种方式:
- 继承Thread类
- 实现Runnable接口
方式一:
- 创建类(线程任务类)继承Thread,并重写run方法
- 创建线程类对象
- 调用线程对象的start方法启动线程
public class Thread1Demo {
public static void main(String[] args) {
//2.创建线程对象
MyThread myThread = new MyThread();
//3.启动线程
myThread.start();
//主线程代码
for (int i = 0; i < 30; i++) {
System.out.println("======主线程=======" + i);
}
}
}
//1.编写线程类,继承Thread类
class MyThread extends Thread{
//重写run方法,即线程的任务
@Override
public void run() {
System.out.println("线程任务启动");
for (int i = 0; i < 30; i++) {
System.out.println("分支线程" + i);
}
}
}
注意事项:
- 不要直接调用run方法来启动线程,run方法是由jvm来调用的
- 不要重复启动同一个线程
方式二:
- 创建类(线程任务类)实现Runnable接口,并重写run方法
- 创建线程类对象
- 创建Thread对象,通过构造器传入线程任务对象
- 调用线程对象的start方法启动线程
public class Thread2Demo {
public static void main(String[] args) {
A a = new A();
Thread thread = new Thread(a);
thread.start();
//主线程代码
//Thread thread = Thread.currentThread();
for (int i = 0; i < 30; i++) {
System.out.println("======主线程=======" + i);
}
}
}
class A implements Runnable{
@Override
public void run() {
//System.out.println("线程任务启动");
for (int i = 0; i < 30; i++) {
System.out.println("分支线程" + i);
}
}
}
注:使用接口创建多个线程,方便共享数据,多个线程可以都使用同一个线程任务类启动线程,在后期线程的同步机制中也更为便于锁的实现。
2. 匿名内部类实现
当创建一个线程,只会使用一次后就不会再使用了,可以使用匿名内部类方式实现。
public class Demo3 {
public static void main(String[] args) {
//方式1匿名写法:
new Thread(){
@Override
public void run() {
System.out.println("线程任务A");
}
}.start();
//方式2匿名写法:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程任务B");
}
}).start();
}
}
3 Thread类
3.1 构造器
- 空参构造器:
public Thread()
- 指定名字:
public Thread(String name)
- 指定线程任务对象:
public Thread(Runnable target);
3.2 基本方法
run()
: 定义线程执行任务isAlive()
: 判断当前线程是否还在活动状态currentThread()
: 返回当前线程对象的引用setName()
:设置线程名称- getName():获取线程名称
- setPriority(): 设置线程优先级
- getPriority(): 获取线程优先级
线程优先级最高10,最低1,主线程默认优先级为5,主线程创建的子线程默认优先级也为5.
3.3 线程控制方法
- start(): 启动线程
- sleep(): 线程休眠
- yield(): 线程礼让,抢到CPU资源后让出给其他线程。
- join(): 线程插队,插入线程执行结束后,被插队线程再执行。
stop(): 强迫线程终止,不安全,已废弃。- interrupt(): 给线程标记为中断状态
- interrupted(): 判断线程的中断状态,判断后会清除中断标记。
- isInterrupted(): 判断线程的中断状态,不会清除中断标记。
注: 礼让有可能并没有礼让成功,如果没有其他线程使用CPU资源,原线程还会继续执行。
3.4 守护线程
线程可以分为两类:用户线程和守护线程。用户线程都结束时,守护线程也会随着用户线程的结束而终止。
设置守护线程的方法:
- 必须在启动线程前设置
- setDaemon(true)
JVM中的垃圾回收器就是一个守护线程,守护的就是程序就是用户线程,程序结束时,守护线程就好自动结束。
三、 线程的生命周期
线程生命周期的五种状态:新建、就绪、运行、阻塞、死亡
JDK将线程状态分为6种。其中就绪和运行合并为可运行状态,阻塞状态分为了三种:定时等待状态、无限等待状态、等待监视器锁。
四、线程安全
使用多个线程模拟多个窗口的卖票过程,保证卖票过程的正常运行。
方式1:继承Thread类
public class Demo {
public static void main(String[] args) {
new TicketSell("窗口1").start();
new TicketSell("窗口2").start();
new TicketSell("窗口3").start();
}
}
class TicketSell extends Thread{
static int num = 100;
public TicketSell(String name) {
super(name);
}
@Override
public void run() {
//int num = 100;//局部变量
while(num > 0) {
System.out.println(Thread.currentThread().getName()+"卖出了第"+num+"张票");
num -- ;
}
System.out.println("=====买完了=====");
}
}
运行结果:出现了重票问题,第100张卖了多次。
方式2:实现Runnable接口
public class Demo2 {
public static void main(String[] args) {
TicketSell2 sell2 = new TicketSell2();
new Thread(sell2, "窗口1").start();
new Thread(sell2, "窗口2").start();
new Thread(sell2, "窗口3").start();
}
}
class TicketSell2 implements Runnable{
static int num = 100;
@Override
public void run() {
//int num = 100;//局部变量
while(num > 0) {
System.out.println(Thread.currentThread().getName()+"卖出了第"+num+"张票");
num -- ;
}
System.out.println("=====买完了=====");
}
}
运行结果:同样出现线程安全问题,这里就不再贴图了。
出现线程安全问题的前提条件
- 多个线程访问
- 访问的是共享数据
- 操作共享数据的过程不是原子操作
解决方法:同步代码块synchronized
出现线程安全问题的前两个条件无法改变,我们就是想让多线程去访问共享数据。故只能对第三个条件进行处理,其中sychronized关键字来将访问共享数据的操作变成原子操作。同步代码块要越小越好,同步代码块越大,效率越低。代码块最小时,应该包含所有操作共享数据的代码,其中本问题中的num
就是共享数据。必须将判断num
大小和对num--
的代码都包含在代码块中才能解决线程安全问题。
public class Demo2 {
public static void main(String[] args) {
TicketSell2 sell2 = new TicketSell2();
new Thread(sell2, "窗口1").start();
new Thread(sell2, "窗口2").start();
new Thread(sell2, "窗口3").start();
}
}
class TicketSell2 implements Runnable{
static int num = 100;
@Override
public void run() {
//int num = 100;//局部变量
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {//this就是锁对象
if(num > 0)//共享数据
System.out.println(Thread.currentThread().getName()+"卖出了第"+num--+"张票");//共享数据
}
if(num <= 0) {
break;
}
}
System.out.println("=====买完了=====");
}
}
锁对象
当线程获取到锁对象才能执行同步代码,在执行完同步代码时会持有锁对象,执行完同步代码后才会释放锁对象。
因此,在使用同步代码块解决线程安全问题时,必须保证锁对象是同一个实例。如果上面代码中的synchronized (this)
更换为new Object()
时就仍然会出现线程安全问题。
- 非静态静态方法,默认的锁对象就是this
- 静态同步代码块建议使用当前类的Class实例作为锁对象
编写多线程程序的一般原则
把共享数据和操作共享数据的方法封装到一个类中,作为共享资源类,与线程类解耦合。还是以买票问题为例子,将该问题中的买票和操作共享数据分成两个类来处理。
共享资源类:
public class Ticket{
private int num = 100;//共享数据
//操作共享数据的方法
public synchronized void sale(){
if(num > 0)
System.out.println("卖出了第" +(num--)+"张票");
}
//get-set方法
}
线程类:
public class TicketThread extend Thread{
private Ticket ticket;//共享资源
public TicketThread(String name, Ticket ticket){
super(name);
this.ticket = ticket;
}
@Override
public void run(){
while(true){
ticket.sale();
if (ticket.getNum() <= 0){
return ;
}
}
}
}
单例模式之懒汉式的同步代码块
在实现单例模式时,懒汉式是先判断当前对象实例是否已经创建了,如果为空就创建一个新的对象,否则就new
一个唯一的实例。如果在多线程的情况下, 由于判断是否为空和创建对象实例不是原子操作,在CPU资源切换时有可能导致单例模式失效的问题,也需要用到同步代码块。
public class Singleton {
//私有的静态变量
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){//如果目前对象还未创建,就进入同步代码块,否则直接返回实例对象即可
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}