1.初识多线程
单线程与多线程:如果程序只有一条执行路径,那么该程序就是单线程程序;如果程序有多条执行路径,那么该程序就是多线程程序。
想要了解多线程,我们就需要先了解线程,想要了解线程,就必须先了解进程,因为线程是依赖于进程的
进程
概述:进程是系统进行资源分配和调用的独立单位,每一个进程都有自己的内存空间和系统资源,简单的说,进程就是正在运行的程序,如网易云音乐,微信,Word等在运行的程序
多进程的意义:
单进程的计算机只能做一种事情,而我们现在的计算机都可以做多种事情,也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务,如边玩游戏边听音乐,多进程的使用可以提高CPU的使用率
问题:多个进程间是同时发生的么?
答:不是,因为单CPU在某一个时间点上只能做一件事情,而多个进程能给我们同时进行的感觉是因为cpu在做着程序间的高效切换,现在的计算机都是多核的了,但是我们的进程往往是比cpu多的,所以,cpu还是需要高效切换来给我们达到同时进行多个进程的感觉
线程
概述:在同一个进程内又可以执行多个任务,而这每一个任务就可以看成是一个线程,线程是程序的执行单元,执行路径,是调用cpu的最小执行单位,单线程就是程序只有一条执行路径,而多线程就是有多条执行路径
多线程的意义:
多线程的存在,不是提高程序的执行速度,其实是为了提高程序的使用率;程序的执行其实都是在抢CPU的资源,CPU的执行权;多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多的话,就会有更高几率抢到CPU的执行权;我们是不敢保证哪一个线程能够在哪个时刻抢到的,所以线程的执行具有随机性
并行与并发的区别
并行是逻辑上的同时发生,指的是在某一个时间内同时运行多个程序;并发则是物理上的同时发生,指在某一个时间点同时运行多个程序
java程序的运行原理
由java命令启动JVM,JVM启动就相当于启动了一个进程,接着由该进程创建一个主线程去调用main方法
问题:jvm虚拟机的启动是单线程的还是多线程的
答:多线程的,因为垃圾回收线程也要先启动,不然很容易导致内存溢出,现在的垃圾回收线程加上前面的主线程,最少启动两个线程,所以说jvm是多线程的
2.如何实现多线程
由于线程是依赖进程存在的,所以我们应该先创建一个进程出来,而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程,java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序,但是java可以去调用C/C++写好的程序来实现多线程程序,由C/C++去调用系统功能创建进程,然后由java去调用这些东西,然后提供一些类供我们使用,我们就可以实现多线程程序了
实现多线程的方法:
- 方式一:继承Thread类
- 方式二:实现Runable接口
- 方式三:实现Callable接口
继承Thread类实现多线程
步骤:
- 自定义类MyThread继承Thread类
- MyThread类里面重写run()方法
- 这个run方法用来包含那些被线程执行的代码
- 创建对象
- 启动线程
run()和start的区别:
run()方法仅仅是封装被线程执行的代码,直接调用是普通方法;start()方法首先启动了线程,然后由jvm去调用该线程的run()方法
获取和设置线程对象的名称的方法(谁先抢到谁就是Thread-0):
- public final String getName()
- public final void setName(String name):设置线程的名称,也可以通过带参构造方法设置线程的名称
针对不是Thrad类的子类中如何获取线程对象名称呢?
- public static Thread currentThread():返回当前正在执行的线程对象
- Thread.currentThread().getName():获取当前正在执行的线程对象的名称
这种方式相比继承Thread类的好处:
- 可以避免由于java单继承带来的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想
代码实现:
//MyThread.java
public class MyThread extends Thread{
@Override
public void run() {
//一般来说,被线程执行的代码肯定是比较耗时的代码,所以我们使用循环
for(int i = 0; i < 200; i++){
System.out.println(getName() + ":" + i);
}
}
}
//MyThreadDemo.java
public class MyThreadDemo {
public static void main(String[] args) {
MyThread mh1 = new MyThread();
MyThread mh2 = new MyThread();
mh1.start();
mh2.start();
}
}
通过实现Runable接口实现多线程(推荐使用)
步骤:
- 自定义类MyRunnable实现Runnable接口
- 重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,并把C步骤的对象作为构造参数传递
代码实现:
//MyRunnable.java
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 100; i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
//MyRunnableDemo.java
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable my = new MyRunnable();
/*Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
//设置线程名字
t1.setName("线程一");
t2.setName("线程二");*/
//与上面代码等价
Thread t1 = new Thread(my, "线程一");
Thread t2 = new Thread(my, "线程二");
t1.start();
t2.start();
}
}
通过实现Callable接口实现多线程
概述:Callable是一个带泛型的接口,通过它做多线程的话需要依赖于线程池(后面回讲线程池)
使用步骤:
1.创建一个线程池对象,控制要创建几个线程对象
- public static ExecutorService newFixedThreadPool(int nThreads):创建有多个线程对象的线程池
2.调用如下方法即可:
Future submit(Callable task) - 返回值Future是一个接口,它的get方法返回计算结果
3.线程池开启之后不会自动关闭,如果想关闭,需要执行以下方法
- public void shutdown():关闭线程池
示例代码1(不带泛型的Callable接口对象代表的多线程)
//Mycallable.java
import java.util.concurrent.Callable;
//不带泛型的Callable对象代表的线程
public class MyCallable implements Callable{
@Override
public Object call() throws Exception {
for(int i = 0; i < 100; i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return null;
}
}
//ExecutorDemo.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsDemo {
public static void main(String[] args) {
//创建一个线程对象,控制要创建几个线程对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//执行Callable对象代表的线程
pool.submit(new MyCallable());
pool.submit(new MyCallable());
//关闭线程池
pool.shutdown();
}
}
实例代码2(带泛型的Callable对象代表的多线程,求和案例)
//MyCallable2.java
import java.util.concurrent.Callable;
//带泛型的Callable对象代表的多线程
public class MyCallable2 implements Callable<Integer>{
private int number;
public MyCallable(int number){
this.number = number;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int x = 1; x <= number; x ++){
sum += x;
}
return sum;
}
}
// ExecutorsDemo2
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ExecutorsDemo2 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建一个线程对象,控制要创建几个线程对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//执行Callable对象代表的线程
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(200));
Integer i1 = f1.get();
Integer i2 = f2.get();
System.out.println(i1);
System.out.println(i2);
//关闭线程池
pool.shutdown();
}
}
3.线程调度
概述:假如我们的计算机只有一个CPU,那么,CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令
线程的两种调度模型:
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么,会随机选择一个,优先级高的线程获得的CPU时间片多一些,java就是使用这种模型
那么,java中程序的优先级是怎么样的,可以设置优先级么?我们接下来就来看看java中的优先级是什么样的
设置和获取线程对象优先级的方法:
- public final void setPriority(int newPriority):更改线程的优先级
- public final int getPriority():返回线程对象的优先级
注意:
- 线程默认优先级是5
- 线程优先级的范围是1-10
- 线程优先级高的仅仅表示线程获取的CPU时间片的几率高,要在次数比较多,或者多次运行的时候,才能看到明显的效果(优先级高的先执行)
线程控制
我们已经知道了线程的调度,接下来我们就可以使用如下方法对象来对线程进行控制
线程休眠
- public static void sleep(long mills):休眠mills毫秒
线程加入
- public static void join():使用了join方法的线程执行完后,才能执行其他线程
线程礼让
- public static void yield():暂停当前正在执行的线程对象,并执行其他线程,它会让多个线程的执行更加和谐一些,但是不能靠他保证一人一次
后台线程(守护线程)
- public final void setDaemon(boolean on):
设置为true时,即设置为守护线程,当守护的线程结束了的话,它们再在cpu上跑一会就得结束线程,而不是完全执行完自己线程的内容再结束
中断线程:
- public final void stop():该方法已经过时了,具有不安全性,不推荐使用
- public void interrupt():中断线程,把线程的状态终止,并抛出一个InterruptException异常
4.线程生命周期
线程的生命周期:
- 新建:创建线程对象
- 就绪:有执行资格,没有执行权
- 运行:有执行资格,有执行权
- 阻塞:由于一些操作让线程处于了该状态,没有执行资格,没有执行权,而另一些操作却可以把它激活,激活后处于就绪状态
- 死亡:线程对象变成垃圾,等待被回收
多线程的状态转换图如下:
5.线程安全问题
再讲解我们的线程安全问题之前,我们先来看一个案例:
三个窗口同时出售一百张电影票的案例:
//MyRunnable2.java
public class MyRunnable2 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
try {
//在真实场景中,会有网络延迟等问题,所以添加了100ms的延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "票");
}else{
break;
}
}
}
}
//Demo2.java
public class Demo2 {
public static void main(String[] args) {
MyRunnable2 my = new MyRunnable2();
Thread t1 = new Thread(my, "窗口1");
Thread t2 = new Thread(my, "窗口2");
Thread t3 = new Thread(my, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
如果你复制这段代码并执行了几次,就会发现,执行结果居然有卖相同张票,第0张票甚至卖负数的票的情况,这是为什么呢?
出现相同票的原因:CPU的一次操作必须是原子性的,
由于我们的程序对ticket变量的操作不是原子性的,这就会导致当我们再卖出某一张票之后,还没等到对ticket变量--,下一个线程就接着卖这同一张票了
出现负票的原因:随机性和延迟导致的,因为有了延迟和随机性,导致三个线程都进去了if判断语句,但是要等100ms,然后再执行下一个,所以就会导致当卖出第1张票之后,还会卖出第0和负数票
通过上面这个案例,我们了解到了多线程的不安全性,那产生线程不安全的原因以及怎么解决,接下来就要正式来说说了
线程安全问题产生的原因(以后我们判断一个程序是否会有线程安全问题的标准):
- 是否有多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
怎么解决线程安全问题
通过看产生线程安全问题的原因,我们可以知道,前两种导致线程不安全的原因我们无法解决,只能解决第三个原因了
思想:把多条语句操作共享数据的代码给包起来,让某个线程在执行的时候,别人不能来执行,而java就给我们提供了这样的方法:同步进制
同步代码块:
synchronized(对象){
需要同步的代码;
}
注意:同步可以解决安全问题的根本原因就在那个对象上,该对象如同锁的功能,而多个线程必须用的是用一把锁
对上面案例的改进:
//MyRunnable3.java
public class MyRunnable3 implements Runnable{
private int ticket = 100;
//创建锁对象
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(ticket > 0){
try {
//在真实场景中,会有网络延迟等问题,所以添加了100ms的延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "票");
}else{
break;
}
}
}
}
}
//Demo3.java
public class Demo3 {
public static void main(String[] args) {
MyRunnable2 my = new MyRunnable2();
Thread t1 = new Thread(my, "窗口1");
Thread t2 = new Thread(my, "窗口2");
Thread t3 = new Thread(my, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
同步的特点
前提:程序是多个线程的
同步的好处:同步的出现解决了多线程的安全问题
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是非常耗费资源的,无形种会降低程序的运行效率,而且同步比较容易产生死锁
同步代码块以及把synchronized关键字加到方法上
1.同步代码块的锁对象是任意对象
2.同步方法的锁对象是this
3.静态方法的锁对象是类的字节码文件对象
6.JDK5的Lock锁
概述:虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并 没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,jdk5之后就提供了一个新的锁对象Lock
Lock:是一个接口,可以通过ReentrantLock具体类来实现
Lock获取锁和释放锁的方法:
- void lock():获取锁
- void unlock():释放锁
使用Lock锁售卖电影票的代码:
// MyRunnable4.java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunnable4 implements Runnable{
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
//获取锁
try{
lock.lock();
if(ticket > 0){
try {
//在真实场景中,会有网络延迟等问题,所以添加了100ms的延迟
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (ticket--) + "票");
}else{
break;
}
}finally{
//释放锁
lock.unlock();
}
}
}
}
//Demo4.java
public class Demo2 {
public static void main(String[] args) {
MyRunnable2 my = new MyRunnable2();
Thread t1 = new Thread(my, "窗口1");
Thread t2 = new Thread(my, "窗口2");
Thread t3 = new Thread(my, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
死锁问题
概述:有两个或两个以上的线程在争夺资源的过程中,发生的一种互相等待的现象
死锁的代码实现:
//MyLock.java
public class MyLock {
public static final Object objA = new Object();
public static final Object objB = new Object();
}
//DieLock
public class DieLock extends Thread{
private boolean flag;
public DieLock(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
}else{
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
//DieLockDemo
public class DieLockDemo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
DieLock dl2 = new DieLock(false);
dl1.start();
dl2.start();
}
}
线程间通信问题
概述:不同种类的线程间针对同一个资源的操作,如生产者消费者模式
生产者消费者模式的实现代码:
//Person.java
public class Person {
private String name;
private int age;
boolean flag = false;//标记位,标记是否有人了,flase代表没人
public Person(){};
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
//Product.java
public class Product implements Runnable{
//这里接受一个Person对象,因为要与消费者共享资源
private Person p;
private int x = 0;
public Product(Person p){
this.p = p;
}
@Override
public void run() {
while(true){
//这里用p对象作为锁,因为生产者和消费者需要使用同一把锁对象来保证线程安全
synchronized (p) {
//假设有人了
if(p.flag){
try {
p.wait();//有人的情况下,就不生产人了,阻塞住,并立即释放锁,将来醒来时,是从这里醒过来的
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没人的情况下,生产人出来
if(x % 2 == 0){
p.setName("卢一");
p.setAge(18);
}else{
p.setName("黄伊");
p.setAge(17);
}
x ++;
////注意这里的修改标记值和唤醒线程必须放在锁里面,如果没有,将会报 java.lang.IllegalMonitorStateException异常
//生产人出来了,就把标记值改为true
p.flag = true;
//唤醒线程
p.notify();
}
}
}
}
//Customer.java
public class Customer implements Runnable {
//这里接受一个Person对象,因为要与生产者共享资源
private Person p;
public Customer(Person p){
this.p = p;
}
@Override
public void run() {
while(true){
//这里用p对象作为锁,因为生产者和消费者需要使用同一把锁对象来保证线程安全
synchronized (p) {
if(!p.flag){
try {
p.wait();//没人的情况下,就阻塞等待,将来醒来从这里醒来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有人了就看看这个人是谁(消费掉)
System.out.println(p.getName() + "-----" + p.getAge());
//注意这里的修改标记值和唤醒线程必须放在锁里面,如果没有,将会报 java.lang.IllegalMonitorStateException异常
//看腻了,想看下一个人(消费掉了),修改标记值为没人
p.flag = false;
//唤醒线程
p.notify();
}
}
}
}
Test.java
public class Test {
public static void main(String[] args) {
//创建资源
Person p = new Person();
Product pro = new Product(p);
Customer c = new Customer(p);
//创建两条不同种类线程
Thread product = new Thread(pro);
Thread customer = new Thread(c);
//启动线程
product.start();
customer.start();
}
}
//结果:
卢一-----18
黄伊-----17
卢一-----18
黄伊-----17
卢一-----18
黄伊-----17
卢一-----18
黄伊-----17
卢一-----18
黄伊-----17
...
从以上的例子中,我们可以知道,即使是不同种类的线程,也需要相同的锁对象来保证数据的安全性;而且为了更符合生成者消费者模式(生产一个就消费一个),我们需要引入等待唤醒机制
等待唤醒的三个方法(是Object类中提供的方法):
- wait():等待
- notify():唤醒单个线程
- notifyAll():唤醒所有线程
为什么这些方法定义不定义在Thread类中,而是Object类中呢?
答:这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象就是任意锁对象,所以,这些方法必须定义在Object类中
7.线程组
概述:把多个线程组合在一起,它可以对一批线程进行分类管理,java运行程序直接对线程进行控制
那么,如果我们想要知道某一个线程所在的线程组,怎么办?
我们可以通过以下方法得到:
- public final ThreadGroup getThreadGroup():获取线程所在的线程组对象,在通过getName()方法即可获得线程组名称
ThreadGroup类的使用:
-
ThreadGroup(String name):构造方法,创建一个线程组对象
- 如:ThreadGroup tg = new ThreadGroup("线程组名");
-
通过对线程组对象进行线程控制,达到对这个线程组里的所有线程进行线程控制的效果
- 如:tg.setDaemon(true):设置了该线程组对象下的所有线程为守护线程
8.线程池
概述:程序启动一个新线程成本是比较高的,因为他涉及到要与操作系统进行交互,而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期比较短的线程时,更应该考虑使用线程池,线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,在jdk5之前,我们必须手动实现自己的线程池,从jdk5之后,java内置支持线程池
使用线程池的步骤:
1.创建一个线程池对象,控制要创建几个线程对象
- public static ExecutorService newCacheThreadPool():创建一个具有缓冲功能的线程池
- public static ExecutorService newFixedThreadPool(int nThreads):创建有多个线程对象的线程池
- public static ExecutorService:造一个线程池,相当于第二个方法里面的nThread值为1 newSingleThreadExecutor():
2.这种线程池的可以执行:
- 实现Runnable接口的对象的线程
- 实现Callable即可的对象的线程
3.调用如下方法即可:
- Feture<?> submit(Runnable task)
Future submit(Callable task) - 返回值Future是一个接口,它的get方法返回计算结果
4.线程池开启之后不会自动关闭,如果想关闭,需要执行以下方法
- public void shutdown():关闭线程池
示例代码:
//MyRunnable.java
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 100; i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
//ExecutorsDemo.java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsDemo {
public static void main(String[] args) {
//创建一个线程对象,控制要创建几个线程对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//执行Runnable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
//关闭线程池
pool.shutdown();
}
}
8.匿名内部类实现多线程程序
示例代码:
public class Demo {
public static void main(String[] args) {
//继承Thread类实现多线程
new Thread(){
@Override
public void run() {
for(int i = 0; i < 100; i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
};
}.start();
//实现Runnable接口实现多线程
new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 100; i ++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}){}.start();
//走的是Thread
new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 100; i ++){
System.out.println("走的是Runnable:" + i);
}
}
}){
@Override
public void run() {
for(int i = 0; i < 100; i ++){
System.out.println("走的是Thread:" + i);
}
}
}.start();
}
}
9.定时器
概述:定时器可以让我们在指定的时间做某件事情,还可以重复做某件事,依赖Timer类和TimeTask这两个类实现
Time:定时
- public Timer():构造方法
- public void schedule(TimeTask task, long delay):延迟一段时间后执行task任务,只执行一次
- public void schedule(TimeTask task, long delay, long period):延迟一段时间后执行task任务,每隔一段时间后再重复执行task任务
- public void schedule(TimeTask task, Date time):安排在指定的时间执行指定的任务
- public void schedule(TimeTask task, Date firstTime, long period):安排指定的任务在指定的时间进行,每隔一段时间再重复执行任务
- public void cancel():取消定时器
代码示例:
import java.util.Timer;
import java.util.TimerTask;
public class TimeDemo {
public static void main(String[] args) {
Timer t = new Timer();
//设置三秒延迟后爆炸,并每隔两秒爆炸一次
t.schedule(new MyTask(), 3000, 2000);
}
}
class MyTask extends TimerTask{
@Override
public void run() {
System.out.println("beng,爆炸了");
}
}
10.学完多线程须知
1.同步有几种方式,分别是什么?
答:两种。同步代码块和同步方法
2.启动一个线程是run()还是start()?他们的区别?
答:run()方法封装了被线程执行的代码,直接调用仅仅是普通方法的调用;start()方法启动线程,并由jvm自动调用run()方法
3.sleep()方法和wait()方法的区别
sleep()方法必须指定时间,不释放锁;wait()方法可以不知道时间,也可以指定时间,释放锁
4.为什么wait(),notify(),notifyAll()等方法定义在Object类中
答:因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁,而Object代表任意的对象,所以,定义在这里面