1. 多线程入门
1.1 多线程相关概念
- 并发与并行
- 并行:在同一时刻,有多个任务在多个CPU上同时执行。
- 并发:在同一时刻,有多个任务在单个CPU上交替执行。
- 进程与线程
- 进程:就是操作系统中正在运行的一个应用程序。
- 线程:就是应用程序中做的事情。比如:360软件中的杀毒,扫描木马,清理垃圾。
1.2 什么是多线程
- 是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
- 好处 : 提高任务的执行性能。
1.3 多线程的创建方式
1.3.1 继承 Thread 的方式
// 基本步骤 : // 1 创建一个类继承Thread类。 // 2 在类中重写run方法(线程执行的任务放在这里) // 3 创建线程对象,调用线程的start方法开启线程。 public class MyThread01 { public static void main(String[] args) { // 创建线程对象,调用线程的start方法开启线程。 MyThread mt = new MyThread(); mt.start(); // main方法中的任务 for (int i = 1; i <= 100; i++) { System.out.println("i:" + i); } } } // 创建一个类继承Thread类。 class MyThread extends Thread { // 在类中重写run方法(线程执行的任务放在这里) @Override public void run() { for (int i = 1; i <= 100; i++) { System.out.println("i:" + i); } } }
1.3.2 实现 Runnable 接口的方式
// 基本步骤 : // 1 定义任务类实现Runnable,并重写run方法 // 2 创建任务对象 // 3 使用含有Runnable参数的构造方法,创建线程对象并指定任务。 // 4 调用线程的start方法,开启线程 public class MyThread02 { public static void main(String[] args) { // 创建线程对象,调用线程的start方法开启线程。 MyRunnable mr = new MyRunnable(); Thread thread= new Thread(mr); thread.start(); // main方法中的任务 for (int i = 1; i <= 100; i++) { System.out.println("i:" + i); } } } // 1 定义任务类实现Runnable,并重写run方法 class MyRunnable implements Runnable { // 在类中重写run方法(线程执行的任务放在这里) @Override public void run() { for (int i = 1; i <= 100; i++) { System.out.println("i:" + i); } } }
1.3.3 实现 Callable 接口的方式
public class Thread3 { public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadThree threadThree = new ThreadThree(); FutureTask task = new FutureTask(threadThree); Thread thread = new Thread(task); thread.start(); //System.out.println(task.get()); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " : " + i); } } } class ThreadThree implements Callable<String> { @Override public String call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " : " + i); } return "end "; } }
1.3.4 Thread 类中常用方法
String getName():返回此线程的名称
Thread类中设置线程的名字
void setName(String name):将此线程的名称更改为等于参数 name
通过构造方法也可以设置线程名称
public static Thread currentThread():返回对当前正在执行的线程对象的引用
public static void sleep(long time):让线程休眠指定的时间,单位为毫秒
线程有两种调度模型
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
1.3.5 sleep() 方法 和 wait() 方法区别:
sleep方法是Thread类的静态方法,wait()是Object超类的成员方法
调用sleep方法的线程不会释放对象锁,而调用wait() 方法会释放对象锁。sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。在调用sleep()方法的过程中,线程不会释放对象锁。
因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
而当调用wait()方法的时候,线程会放弃对象锁,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。
sleep方法需要抛异常,wait方法不需要
sleep方法可以在任何地方使用,wait方法只能在同步方法和同步代码块中使用
2. 线程安全
2.1 线程安全产生的原因
多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了
举例:略
问题出现的原因 : 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了
2.2 线程的同步
概述 : java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性
分类
同步代码块
同步方法
锁机制,Lock
2.3 同步代码块
同步代码块 : 锁住多条语句操作共享数据,可以使用同步代码块实现
第一部分 : 格式
synchronized(任意对象) { 多条语句操作共享数据的代码 }
第二部分 : 注意
1 默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
2 当线程执行完出来了,锁才会自动打开
第三部分 : 同步的好处和弊端
好处 : 解决了多线程的数据安全问题
弊端 : 当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
注意:当该多线程类实现方式是继承Thread时,创建多个线程对象的时候,并且锁对象是 this 的时候 那么这个锁对象其实不是唯一的,会有问题滴。
public class Ticket implements Runnable { private int ticketCount = 100; // 一共有一百张票 @Override public void run() { while (true) { synchronized (Ticket.class) { // 如果票的数量为0 , 那么停止买票 if (ticketCount <= 0) { break; } else { // 模拟出票的时间 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 有剩余的票 , 开始卖票 ticketCount--; System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张"); } } } } }
2.4 同步方法
同步方法:就是把synchronized关键字加到方法上
格式:修饰符 synchronized 返回值类型 方法名(方法参数) { }
同步代码块和同步方法的区别:
1 同步代码块可以锁住指定代码, 同步方法是锁住方法中所有代码
2 同步代码块可以指定锁对象, 同步方法不能指定锁对象
注意 : 同步方法时不能指定锁对象的 , 但是有默认存在的锁对象的。
1 对于非 static 方法, 同步锁就是this。
2 对于 static 方法, 我们使用当前方法所在类的字节码对象(类名.class)。 Class类型的对象
/* 同步方法:就是把synchronized关键字加到方法上 格式:修饰符 synchronized 返回值类型 方法名(方法参数) { } 同步代码块和同步方法的区别: 1 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码 2 同步代码块可以指定锁对象,同步方法不能指定锁对象 注意 : 同步方法时不能指定锁对象的 , 但是有默认存在的锁对象的。 1 对于非static方法,同步锁就是this。 2 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。 Class类型的对象 */ public class Ticket implements Runnable { private int ticketCount = 100; // 一共有一百张票 @Override public void run() { while (true) { if (method()) { break; } } } private synchronized boolean method() { // 如果票的数量为0 , 那么停止买票 if (ticketCount <= 0) { return true; } else { // 模拟出票的时间 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 有剩余的票 , 开始卖票 ticketCount--; System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张"); return false; } } }
2.5 Lock 锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,SO ,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock 中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁
Lock 是接口不能直接实例化,这里采用它的实现类 ReentrantLock 来实例化
ReentrantLock 的构造方法
ReentrantLock():创建一个 ReentrantLock 的实例
注意:多个线程使用相同的 Lock 锁对象,需要多线程操作数据的代码放在 lock() 和 unLock()方法之间。一定要确保 unlock 最后能够调用
import java.util.concurrent.locks.ReentrantLock; /* 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁, 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock Lock中提供了获得锁和释放锁的方法 void lock():获得锁 void unlock():释放锁 Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化 ReentrantLock的构造方法 ReentrantLock():创建一个ReentrantLock的实例 注意:多个线程使用相同的Lock锁对象,需要多线程操作数据的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用 */ public class Ticket implements Runnable { private int ticketCount = 100; // 一共有一百张票 private static ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { lock.lock();// 加锁 // 如果票的数量为0 , 那么停止买票 if (ticketCount <= 0) { break; } else { // 模拟出票的时间 Thread.sleep(100); // 有剩余的票 , 开始卖票 ticketCount--; System.out.println(Thread.currentThread().getName() + "卖出一张票,剩下" + ticketCount + "张"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock();// 释放锁 } } } }