多线程
概述
线程是依赖于进程而存在的
进程
正在运行的程序,是系统进行资源分配和调用的独立单位
每一个进程都有他自己的内存空间和系统资源
线程
是进程中的单个顺序控制流,是一条执行路径
一个进程如果只有一个执行路径,则被称为单线程程序
一个进程如果有多条执行路径,则被称为多线程程序
意义
多线程的作用不是提高执行速度,而是为了提高应用程序的使用率
而多线程却给了我们一个错觉,让我们认为多个线程是并发执行的,其实不是
因为多个线程共享同一个进程的资源(堆内存和方法区),但是栈内存是独立的,一个线程一个栈。所以他们仍然是在抢CPU的资源执行,一个时间点上只有能有一个线程执行。而且谁抢到,这个不一定,所以,造成了线程运行的随机性
并发和并行
并发:逻辑上同时发生,指在某一个时间段内同时运行多个程序
并行:物理上同时发生,指在 某一个时间点同时运行多个程序
Java程序运行原理
java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法。所以main方法运行在主线程中。在此之前的所有程序都是单线程的。
注:JVM的启动至少启动了垃圾回收线程和主线程,所以是多线程
线程调度
如果我们的计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。那么java是如何对线程进行调用的呢
线程有两种调度模型:
分时调度模型
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
抢占式调度模型
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一点
java使用的是抢占式调度模型
正常代码程序内的优先级默认是5
public final int getPriority() 返回此线程的优先级
public final void setPriority(int newPriority) 更改此线程的优先级
参数newPriiority的范围为1-10之间
总结:
1、线程的默认优先级是5
2、线程优先级的范围是1-10
3、线程优先级高仅仅表示的是获取CPU时间片的几率会高一些,但不是绝对一定会先执行
线程控制
线程休眠
millis是毫秒,1s=1000毫秒
public static void sleep(long millis)
线程加入(插队)
其他线程等待这个线程死亡
在线程设置为加入线程之前,先将该线程变为就绪状态,也就是调用start()方法
public final void join()
线程礼让
暂停当前正在执行的线程对象,并执行其他线程
他的作用是为了让多个线程之间更加和谐一点,并不能一定保证多个线程一人一次执行
public static void yield()
后台线程(守护线程)
Java中有两类线程:用户线程、守护线程
用户线程:我们在学习多线程之前所有程序代码,运行起来都是一个个的用户线程
守护线程:所谓的守护线程,指的就是程序运行的时候在后台提供了一种通用服务的线程,比如,垃圾回收机制,他就是一个称职的守护者,而且这种线程并不属于程序不可获取的部分。所以当非守护线程结束的时候,程序也就终止了,同时会杀死进程中所有的守护线程
反过来说,只要程序中存在非守护线程,程序就不会终止
public final void setDaemon(boolean on) 通过这方法将该线程对象标记为守护线程或者非守护线程
当运行程序中只有一个线程且是守护线程的时候,java虚拟机退出
注意:将线程设置为守护线程这一步骤,必须在启动前设置
中断线程
public final void stop() 让正在运行的线程停止,这个方法过时了
public void interrupt() 中断正在运行的线程,被中断的线程将run方法执行完毕
多线程的实现
第一种方式
继承Thread类
步骤
1、自定义一个子类MyThread继承Thread类
2、自定义类重写Thread类中的run()方法
为什么要重写run方法
因为只有当需要被线程执行的时候,再把需要的代码加入到run()方法里
3、创建子类对象
如何获取和设置线程名字
通过构造方法给线程起名字:
Thread(String name)分配一个新的Thread对象
通过方法给线程起名字:
void setName(String name) 将此线程的名称更改为等于参数name
void getName() 获取此线程的名称
4、启动线程
/*
调用run()与调用start()的区别
run()的调用仅仅是封装了被线程执行的代码,但是直接调用的话是普通的方法调用
start()方法的调用,首先单独启动了一个线程,然后再由JVM去调用该线程的run()方法
*/
//范例
public class Test01 {
public static void main(String[] args) {
MyThread mt01 = new MyThread("小A");
MyThread mt02 = new MyThread("小B");
mt02.setName("小C");
mt01.start();
mt02.start();
}
class MyThread extends Thread{
static int i=10;
public MyThread() {
super();
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while(i>0){
System.out.println(this.getName()+"---"+i--);
}
}
}
}
如何获取main的主线程呢?
public static Thread currentThread() 返回对当前正在执行的线程对象的引用
第二种方法
实现Runnable接口库
1、创建自定义类实现Runnable接口
2、重写un()方法
3、创建自定义类的对象
4、创建Thread类的对象,将第三步创建的自定义类对象作为参数传递到构造方法中
public class MyThread01 implements Runnable{
int i=10;
@Override
public void run() {
while (i>0){
System.out.println(Thread.currentThread().getName()+"----"+i--);
/*
由于事先的Runnable接口中并没有getName()方法,所以这里无法使用Thread类中的
getName()方法
间接调用,因为currentThread()是静态的,可以直接通过类名调用获取当前正在
执行run方法的线程对象(Thread类型),所以可以调用getName()
*/
}
}
}
public class Test02 {
public static void main(String[] args) {
MyThread01 my = new MyThread01();
// Thread t = new Thread(new Runnable() {
// @Override
// public void run() {
// }
// });
/*
实际上在这种模拟多个线程访问共享资源的时候是不能这样干的。
因为匿名内部类里边访问外部的变量,实际上都必须是final类型的变量,
而final修饰的变量是线程安全的。因此也就模拟不出来出错的结果了。
*/
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
第三种方法
实现Callable接口,重写call方法,结合线程池运行
<T> Future<T> submit(Callable<T> task)
提交值返回任务以执行,并返回代表任务待处理结果的Future
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName());
return null;
}
}
public class Test04 {
public static void main(String[] args) {
//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
MyCallable myCallable = new MyCallable();
//把任务提交给线程池,然后线程池划出一条线程来运行
pool.submit(myCallable);
pool.submit(myCallable);
//关闭线程池
pool.shutdown();
}
}
线程的生命周期
同步
概述
用来解决线程安全问题(比如,卖票)
导致线程安全问题的三个因素:(同时满足即是线程安全问题)
1、是否存在多线程环境
2、是否存在共享数据/共享变量
3、是否有多条语句操作着共享数据、共享变量
同步代码块
格式
synchronized(对象){
需要同步的代码;
}
对象:随便一个对象(即是锁)
需要同步的代码:操作共享对象的代码块
同步的前提
多个线程
多个线程的是同一个锁对象
同步的好处
同步的出现解决了多线程的安全问题
同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
同步方法
就是把同步关键字加到方法上
同步方法的锁对象默认是this
如果是静态同步方法,锁对象是该静态同步方法所属那个类的字节码文件(.class)
字节码文件也属于一种对象(反射部分)
//静态同步方法处理
public class MyThread01 implements Runnable{
static int ticket=10;
//Object object=new Object();
@Override
public void run() {
while(ticket>0) {
if (ticket % 2 == 0) {
synchronized (MyThread01.class) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + ticket--);
}
}
} else {
fun();
}
}
}
public static synchronized void fun(){
//synchronized(this) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + ticket--);
}
//}
}
}
public class Test01 {
public static void main(String[] args) {
MyThread01 mt = new MyThread01();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
}
}
lock锁的使用
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更加清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock的具体子类ReentrantLock
void lock() 获得锁
void unlock() 释放锁
如何获取线程安全的集合
/*
Vector虽然是线程安全的,但是我们今后开发也不用他,那用谁呢
Collections工具类
该工具类下面方法得到的所有集合都可以是线程安全的
*/
//创建一个线程安全的ArrayList
ArrayList<String> list=new ArrayList<>();
List<String> strings=new Collections.synchronizedList(List);
strings.add("ss");
strings.add("bbb");
for(String s:strings){
System.out.println("s");
}
死锁问题
如果出现了同步嵌套,就容易出现死锁问题
指两个或者两个以上的线程在执行的过程中争夺资源产生的一种互相等待现象
举例:
中国人,美国人吃饭的问题
正常情况下:
中国人:两支筷子
美国人:一把刀,一把叉子
现在:
中国人:一支筷子,一把刀
美国人:一支筷子,一把叉子
两个线程相互等待别人给资源
线程间通信
概述
1、针对同一个资源的操作有不同种类的线程
举例:卖票有进的,也有出的
2、通过设置线程(生产者)和获取线程(消费者)针对同一个对象进行操作
正常情况下应该是:
1、对于生产者来说:
先看资源有没有数据,如果没有数据,就生产数据,生产完之后,通知消费者过来消费数据,当消费者消费到某一时刻,继续生产
2、对于消费者来说:
先看资源有没有数据,如果有数据,就消费数据,如果没有,就等待着数据产生,通知生产者生产数据
但是在程序里,由于线程之间相互抢占资源就会出现生产消费关系混乱的情况
解决方法:等待唤醒机制
等待唤醒机制
Object类中有三个方法
void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程
至于为什么这些方法不定义在Thread类中呢?
这些方法想要调用,必须通过锁对象进行调用,而我们知道锁对象可以是任意锁对象,所以这些方法,就必须定义在Object类中
等待唤醒机制的实现
public class Student {
private String name;
private int age;
boolean flag; //用一个flag表示生产状态,true表示生产完成,false表示需要生产
public Student() {
}
public Student(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;
}
}
//生产者
public class SetThread implements Runnable{
Student student;
public SetThread(Student student) {
this.student = student;
}
@Override
public void run() {
while(true) {
synchronized (student) {
if (student.flag) {
try {
student.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
student.setName("小二");
student.setAge(20);
student.flag = true;
student.notify();
}
}
}
}
//消费者
public class GetThread implements Runnable{
Student student;
public GetThread(Student student) {
this.student = student;
}
@Override
public void run() {
while(true) {
synchronized (student) {
if (!student.flag) {
try {
student.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(student.getName() + "---" + student.getAge());
student.flag = false;
student.notify();
}
}
}
}
public class Test02 {
public static void main(String[] args) {
Student st = new Student(null,0);
Thread t1 = new Thread(new SetThread(st));
Thread t2 = new Thread(new GetThread(st));
t2.start();
t1.start();
}
}
线程的状态转换
线程组
把多个线程组合到一起
java中使用ThreadGroup来表示线程组
他可以对一批线程进行分类管理
java允许程序直接对线程组进行控制的
public class Test03 {
public static void main(String[] args) {
MyThread03 mt = new MyThread03();
Thread t1 = new Thread(mt,"小A");
Thread t2 = new Thread(mt, "小B");
//默认情况下,所有的线程都属于主线程组
//public final ThreadGroup getThreadGroup 返回该线程所在的线程组
ThreadGroup tg1 = t1.getThreadGroup();
ThreadGroup tg2 = t2.getThreadGroup();
//返回线程组的名称
System.out.println(tg1.getName());
System.out.println(tg2.getName());
System.out.println(Thread.currentThread().getThreadGroup().getName());
/*
Thread(ThreadGroup group,Runnable target, String name)
给线程分组
先创建一个新的线程组
ThreadGroup(String name)
构造一个新的线程组
*/
ThreadGroup tg3 = new ThreadGroup("新的线程组");
//通过线程组,创建线程对象
Thread t3 = new Thread(tg3, mt, "新线程组第一个线程");
Thread t4 = new Thread(tg3, mt, "新线程组第二个线程");
System.out.println(t3.getThreadGroup().getName()+"---"+t3.getName());
System.out.println(t4.getThreadGroup().getName()+"---"+t4.getName());
//直接通过组名设置一个组都是守护线程组,组里面的线程都是守护线程
tg3.setDaemon(true);
}
}
线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
实现线程池
1、创建线程池对象
Executors工厂类下的静态方法,newFixedThreadPool是其中一种线程池
public static ExecutorsService newFixedThreadPool(int nThreads)
方法返回值是ExecutorService对象,该对象表示是一个线程池,可以执行Runnable对象或者Callable对象代表的线程
2、往线程池中放线程
3、在线程池中的线程如何运行
4、结束线程运行
//创建线程池
ExecutorService pool=Executors.newFixedThreadPool(2);
//创建实现Runnable的对象
MyRunnable mr=new MyRunnable();
//Future<?> submit(Runnable task)
//提交一个可运行的任务执行,并返回一个表示该任务的未来
pool.submit(mr);
pool.submit(mr);
//结束线程池
//线程池已经被关闭,不能再重复提交运行
pool.shutdown;
需要了解的线程池
public static ExecutorService newCachedThreadPool()
创建一个具有缓存功能的线程池
缓存:百度浏览过的信息再次访问
public static ExecutorService newFixedThreadPool(int nThreads)
创建一个可重用的,具有固定线程数的线程池
public static ExecutorService newSingleThreadExecutor()
创建一个只有单线程的线程池,相当于上个方法的参数是1
newScheduledThreadPool()
匿名内部类方式使用多线程
匿名内部类的形式创建线程并启动
new Thread(){代码...}.start();
new Thread(new Runnable(){代码...}).start();
多线程总结
多线程:
指的一个程序中有多条执行路径的情况,我们称之为多线程程序
多线程的实现方式:
1、继承Thread类,重写run()方法,调用start()方法启动
2、实现Runnable接口,重写run()方法,创建Thread对象把Runnable对象当作参数传递,
调用start()方法启动
3、实现Callable接口,重写call()方法,提交到线程池中运行,需要结合线程池
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor()
newScheduledThreadPool
线程的同步安全问题:
造成线程同步问题的原因:(三个条件同时存在)
1、是否存在多线程环境
2、是否存在共享数据/共享变量
3、是否有多条语句操作共享数据/共享变量
解决线程的同步安全问题方案
方案一:
同步代码块:
格式:synchronized(锁对象){
需要同步的代码(操作共享数据/共享变量的代码块)
}
锁对象的使用:
注意:发生同步安全的多线程之间的锁对象要一致,锁对象唯一
正常情况下:锁对象是任意对象
同步方法:锁对象是this
同步静态方法:锁对象是当前类的字节码文件对象
方案二:
Lock锁,JDK1.5之后出现
lock() 加锁
unlock() 释放锁
生产者消费者问题:
解决方案:
等待唤醒机制:(前提是线程没有同步安全问题)
Object类下提供了三个方法:
wait()
notify()
notifyAll()
线程组:将多个线程分组,放到一组,程序可以直接操作线程组(比如设置守护线程)
线程池:
好处:
线程池里的每一个线程代码结束后,并不会死亡,
而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
匿名内部类的形式创建线程对象并启动
定时器
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在java中,可以通过Timer和TimerTask类来实现定义调度的功能
Timer
public Timer() //创建一个新的计时器
public void schedule(TimerTask task,long delay) //在指定的延迟之后安排指定的任务执行
public void schedule(TimerTask task,long delay,long period) //在指定的延迟之后开始,重新执行固定延迟执行的指定任务
TimerTask
public abstract void run() //继承之后需重写该方法
public boolean cancel() //终止此计时器,丢弃任何当前计划的任务
public class Task01 extends TimerTask {
private Timer timer;
public Task01(Timer timer) {
this.timer = timer;
}
@Override
public void run() {
System.out.println("我打印了一条数据");
timer.cancel(); //关闭timer
}
}
public class Test01 {
public static void main(String[] args) {
Timer timer = new Timer();
//创建定时器
Task01 task = new Task01(timer);
//创建任务
//timer.schedule(task,3000);
//任务3s后运行
//timer.cancel(); 如果写在外面任务就直接停止,应当写在任务内部
timer.schedule(task,3000,2000);
//任务3s后运行,且每隔2s运行一次
}
}