这里写目录标题
进程与线程
- 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是应用程序运行的载体。
- 线程是一个进程中的执行场景,一个进程由一个或多个线程组成,是进程中的基本执行单元,是一个进程中代码的不同执行路线。
- 线程共享一个进程下面的资源,可以互相通信和影响
区别
-
进程之间数据不共享
-
线程之间可以共享资源
多线程的基本概念
假设启动100个线程,就会有100个栈空间,栈与栈之间互不干扰,各自独立执行,这就是多线程并发。
- 对于多线程来说,堆内存、方法区内存共享,栈内存独立,一个线程一个栈。进程与进程之间也是内存独立不共享。
单核cpu和多核cpu
- 都是一个cpu,不同的是每个cpu上的核心数
- 多核cpu是多个单核cpu的替代方案,多核cpu减小了体积,同时也减少了功耗
- 一个核心只能同时执行一个线程,对于多核cpu,多线程并发是没有问题的。
并发与并行
并行:每个线程分配给独立的核心,线程同时运行,多个任务之间是不相互抢占资源的,只有在多个CPU或CPU多核时,才会发生并行。
并发:多个线程在单个核心运行,同一时间一个线程运行,系统不停切换线程,由于CPU处理速度快,看起来像同时运行,实际上是线程不停切换,多个任务之间是相互抢占资源的。
- java程序中至少有两个线程并发,一个是执行main方法的主线程,一个是垃圾回收线程。
多线程的优点:可以同时完成多个任务;可以使程序的响应速度更快,提高程序的处理效率。
线程的创建与启动
继承Thread类(方式一)
实现多线程的第一种方式:
编写一个类,继承Thread类,重写run,最后在主类采用start方法
start方法作用:启动一个分支线程,在JVM开辟一个新的栈空间,成功开辟新的栈空间后,start方法就结束了,线程就启动了,启动成功的线程就会自动调用run方法,并且run方法在分支栈的栈底部(压栈)
如果主类直接调用run方法会当成普通方法执行,只有调用start方法才是启动一个新的线程执行
编写主类,创建线程对象,启动线程
public class test01 {
public static void main(String[] args) {
//创建线程对象
MyThread myThread = new MyThread();
//启动线程
//start方法作用:启动一个分支线程,在JVM开辟一个新的栈空间,成功开辟新的栈空间后,start方法就结束
//线程就启动了
//启动成功的线程就会自动调用run方法,并且run方法在分支栈的栈底部(压栈)
myThread.start();
//这里的代码还是运行在主程序中
for (int i = 0; i < 10; i++) {
System.out.println("主线程-->"+i);
}
}
}
编写的MyThread类:重写run方法,继承的父类Thread中的run方法没有抛出异常,那么重写的run方法也不能抛出异常
class MyThread extends Thread {
@Override
//重写run方法
public void run() {
super.run();
for (int i = 0; i < 10; i++) {
System.out.println("分支线程-->"+i);
}
}
}
运行结果:
优点:简单便捷
缺点:编写的类只能继承Thread,不能继承其他的,不便于拓展
实现Runnable接口(方式二)
编写一个类实现Runnable接口
编写主类,创建线程对象,启动线程
public class test02 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程-->"+i);
}
}
}
编写一个类,实现Runnable接口,重写run方法
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("分支线程-->"+i);
}
}
}
运行结果:
匿名内部类实现Runnable接口
创建线程对象的时候采用匿名内部类方式
public class test03 {
public static void main(String[] args) {
//创建线程对象
//采用匿名内部类
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
System.out.println("分支线程-->"+i);
}
}
});
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程-->"+i);
}
}
}
优点:这个方法比较常用,更灵活,编写一个类实现接口,还能实现别的类。
使用Callable和Future(方式三)
定义一个MyCallable类,实现Callable接口,重写call方法(有返回值)。
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println("分支线程-->"+i);
}
return "执行成功";
}
}
在主类创建MyCallable对象;创建Future的实现类对象,把MyCallable对象作为参数传进去;创建Thread对象,把Future的实现类对象作为参数传进去;启动线程。
public class test04 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建Future的实现类对象FutureTask,把MyCallable对象作为构造方法的参数
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
//创建Thread类的对象,把FutureTask作为构造方法的参数
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
System.out.println(futureTask.get());
}
}
与第二种实现Runable方法不同的是,重写call方法有返回值,而实现Runable方法中重写run方法没有返回值。
线程池
创建一个池子(装线程的东西),池子中是空的;有任务需要执行时,才会创建线程对象,当任务执行完毕,线程对象归还给池子。
使用线程池创建(方法四)
Executors–默认线程池
1、创建线程池、池子是空的:
类 Executors:帮助创建线程池对象
返回类型 ExecutorService:帮助控制线程池对象
//创建一个默认的线程池
static ExecutorService newCachedThreadPool();
//创建一个指定最多数量的线程池
static ExecutorService newFixedThreadPool(int nThreads);
2、有任务需要执行时,创建线程对象,任务执行完毕,线程对象归还给池子:submit();
3、所有任务执行完毕、关闭连接池shutdown();
代码实现:创建一个默认的线程池
public class test10 {
public static void main(String[] args) {
//创建一个默认的线程池对象,池子中是空的,默认最多容纳int类型的最大值
ExecutorService executorService = Executors.newCachedThreadPool();
// 有任务需要执行时,创建线程对象,任务执行完毕 线程归还给池子
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"正在运行");
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第二个任务 如果池子里面有线程 则不用创建
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"正在运行");
});
//关闭线程
executorService.shutdown();
}
}
代码实现:创建一个指定最多数量的线程池
public class test11 {
public static void main(String[] args) {
//初始化线程池 最多容纳10个线程
ExecutorService executorService = Executors.newFixedThreadPool(10);
//线程池当中有多少线程 初始是0 getPoolSize()
//验证 强转
ThreadPoolExecutor count1 = (ThreadPoolExecutor) executorService;
System.out.println(count1.getPoolSize());//0
//有任务需要执行时,创建线程对象,任务执行完毕 线程归还给池子
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"正在执行任务");
});
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"正在执行任务");
});
//执行完任务后 线程池当中有多少线程 getPoolSize()
//验证 强转
ThreadPoolExecutor count2 = (ThreadPoolExecutor) executorService;
System.out.println(count2.getPoolSize());//2
//关闭连接池
executorService.shutdown();
}
}
运行结果:
ThreadPoolExecutor --创建线程池对象
ThreadPoolExecutor 自己的创建线程池;
构造方法中参数比较多:
参数一:核心线程数量
参数二:最大线程数量
参数三:空闲线程最大存活时间
参数四:空闲线程最大存货时间单位 TimeUnit包下
参数五:任务队列 new ArrayBlockingQueue<>();
参数六:创建线程工厂 Executors.defaultThreadFactory()
参数七:任务的拒绝策略 new ThreadPoolExecutor.AbortPolicy()
public static void main(String[] args) {
//自己创建的线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 10, 2, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() .getName()
+" 正在执行任务1");
}
}); pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() .getName()
+" 正在执行任务2");
}
}); pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() .getName()
+" 正在执行任务3");
}
});
}
任务的拒绝策略:当线程池里面的线程全部都在执行任务,任务队列已经满了的时候,还有多余的任务,公式:(提交的任务数量>线程池最大线程数+队列容量),触发任务的拒绝策略。
可以看出四种任务拒绝策略的方法:
- ThreadPoolExecutor.AbortPolicy();
丢弃任务并抛出异常 RejectedExecutionException 默认策略 - ThreadPoolExecutor.DiscardPolicy();
丢弃任务 不抛异常 不推荐使用 - ThreadPoolExecutor.DiscardOldestPolicy();
丢弃队列中等待时间最久的任务 然后把当前任务加如队列 不抛异常 - ThreadPoolExecutor.CallerRunsPolicy();
调用任务的run方法 绕过线程池执行任务
线程的生命周期
线程的生命周期存在五个状态:
- 新建:采用new语句创建线程对象
- 就绪:执行start方法
- 运行:占用CPU时间
- 阻塞:比如执行了输入语句、sleep语句
- 死亡:退出run方法
首先使用new运算符将线程对象创建出来,此时线程对象处于新建的状态;
start方法启动线程,启动之后线程进入就绪状态,进入就绪状态的线程拥有抢夺CPU时间片的能力;
线程抢到CPU时间片后执行run方法,标志着线程进入运行状态,时间片用完之后,重新回到就绪状态继续抢夺CPU时间片,抢到之后,再次进入运行状态执行run方法;
在运行状态时遇到阻塞事件,线程会放弃之前占有的CPU时间片,让给其他线程使用,之后进入阻塞状态,一旦阻塞解除,重新回到就绪状态抢夺CPU时间片。
run方法执行结束代表线程周期结束即死亡。
线程的常用方法
设置获取名字
修改、获取线程名称:setName、getName方法
//修改线程的名字
//获取线程的名字
myThread.setName("myThread");
System.out.println(myThread.getName());
//默认线程名为 Thread-0
获取线程对象
获取当前线程对象:Thread.currentThread()方法
//获取当前线程对象
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
sleep休眠方法
让当前(如果在主线程调用,那么主线程进入休眠)线程进入休眠,进入阻塞状态放弃占用cpu时间片,让给其他线程使用:Thread.sleep(毫秒)是一个静态方法,有异常处理机制
//休眠3秒
try {
Thread.sleep(1000*3);
} catch (InterruptedException e) {
e.printStackTrace();
}
但是如果编写的线程类继承Runnable、Thread,那么类中的run方法不能在声明位置上使用throws抛出异常,因为父类没有抛,所以子类也不能抛,只能try catch捕捉异常.
打断休眠方法
如果需要打断睡眠状态,就需要interrupt方法干扰睡眠,使从睡眠状态苏醒。interrupt方法
public class test05 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable01());
thread.setName("t");
//启动线程
thread.start();
//因为执行到run方法 发现休眠一天 所以要求5秒后醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程的睡眠
thread.interrupt();
}
}
class MyRunnable01 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->开始");
//休眠一天
//捕捉到异常
try {
Thread.sleep(1000*60*60*24);
} catch (InterruptedException e) {
System.out.println("---睡眠被打断---");
}
//休眠一天才执行到这里
System.out.println(Thread.currentThread().getName()+"-->结束");
}
}
合理的终止线程的执行。 采用修改布尔标记来操作线程的终止!
public class test06 {
public static void main(String[] args) {
MyRunnable02 r = new MyRunnable02();
Thread thread = new Thread(r);
//线程开始
thread.start();
//模拟两秒终止线程
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
r.run = false;
}
}
class MyRunnable02 implements Runnable {
//打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (run) {
System.out.println(Thread.currentThread().getName() + "-->"+i);
//休眠一天
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
return;
}
}
}
}
线程的调度与控制
- 分时调度模型:所以线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先级高的线程获取CPU的时间片多一些,如果线程的优先级相同,则随机选一个。(Java采取)
设置线程的优先级:
void setPriority(int newPriority);
获取线程的优先级
int getPriority();
线程优先级分三种:
- MAX_PRIORITY 最高级:10
- MIN_PRIORITY 最低级:1
- NOM_PRIORITY 默认级:5
优先级比较高的获取CPU时间片可能会多一些,抢夺到CPU执行权的几率更高,不是线程执行一定优先
示例如下:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class test07 {
public static void main(String[] args) {
//创建第一个线程
MyCallable01 c1 = new MyCallable01();
FutureTask<String> futureTask = new FutureTask<>(c1);
Thread thread1 = new Thread(futureTask);
thread1.setName("第一线程");
//设置默认值10 高级
thread1.setPriority(10);
System.out.println(thread1.getPriority());
thread1.start();
//创建第二个线程
MyCallable01 c2 = new MyCallable01();
FutureTask<String> futureTask2 = new FutureTask<>(c2);
Thread thread2 = new Thread(futureTask2);
thread2.setName("第二线程");
thread2.setPriority(4);
System.out.println(thread2.getPriority());
thread2.start();
}
}
class MyCallable01 implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 7; i++) {
System.out.println(Thread.currentThread().getName()+"----"+i);
}
return "线程执行完毕";
}
}
第二线程优先级设置的低,但是执行却优先于线程一。
守护线程
守护线程是什么?就是守护普通线程的执行,当普通线程执行完毕后,守护线程没有再执行的必要,但是守护线程不会立马停止 ,还占有CPU的执行权,CPU执行相率非常快,所以还会再执行一会
public class test08 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("普通线程");
t2.setName("守护线程");
//设置t2守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
线程安全问题
多线程操作共享数据可能会出现线程安全问题;
把多条语句操作共享数据的代码锁起来,让任意时刻执行一个线程;
同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
- 格式
//锁对象是唯一的
synchronized (任意对象){
多条语句操作共享数据的代码
}
- 默认情况是打开的,只要有一个线程进去执行代码,锁就会关闭。
- 线程执行完毕,锁就会自动打开
- 优点:解决多线程的数据安全问题
- 缺点:当线程很多的时候,每个线程都去判断同步上的锁,还有同步的代码越多,执行的时间越长,其他线程等待时间就会越长,耗费资源,降低运行效率
同步方法
把关键字 synchronized 加到方法上。
与同步代码块不同的是:
- 同步代码块锁的是指定代码,同步方法锁的是方法中所有代码
- 同步代码块可以锁指定对象,同步方法不能指定锁对象
- 同步方法的锁对象是this
Lock锁
使用synchronized无法清晰的看到什么时候加锁、什么时候释放锁,为了更清晰的表达如果加锁、释放锁,Lock中提供了两个方法,获得锁 void lock()
、释放锁void unlock()
。
Lock是接口,不能new对象,采用实现类ReentrantLock实例化
死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。也就是锁的嵌套所导致的。
避免死锁!避免锁的嵌套!
案例—电影院卖票
电影院售卖100张票,两个窗口,售卖一张票休息1秒,设计程序模拟电影院卖票;
分析:
- 定义卖票的类–Ticket类继承Runnable接口,重写run方法,定义成员变量–票的数量100,如果票数量=0,输出“ 售空 ”,如果大于0,就让票数减一,并输出哪个窗口售出的票,还剩余多少张票,窗口一使用同步方法、窗口二使用同步代码块。
class Ticket implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if ("窗口一".equals(Thread.currentThread().getName())) {
boolean b = Method01();
if(b){
break;
}
}
if ("窗口二".equals(Thread.currentThread().getName())){
//如果同步方法加了static 那么this就要改成类名.class
synchronized (this){
if(ticket == 0 ){
break;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"正在售卖票,剩余票数"+ticket+"张");
}
}
}
}
}
public synchronized boolean Method01 (){
if(ticket == 0 ){
return true;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"正在售卖票,剩余票数"+ticket+"张");
return false;
}
}
}
- 定义测试类,newTicket对象,创建三个线程,注意只new1个Ticket对象,传入参数,设置线程名字,启动线程。
public class Demo {
public static void main(String[] args) {
//两个窗口卖同一个100张票,所以参数对象创建一个,传入两个线程
Ticket ticket = new Ticket();
//创建两个线程
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
//设置名称
t1.setName("窗口一");
t2.setName("窗口二");
//启动线程
t1.start();
t2.start();
}
}