进程:进行资源分配和调度的一个独立单位。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
多线程应用场景
主要能体现到多线程提高程序效率。举例: 迅雷多线程下载、分批发送短信等。
多线程创建方式
第一种继承Thread类 重写run方法
class CreateThread extends Thread { // run方法中编写 多线程需要执行的代码 public void run() { for (int i = 0; i< 10; i++) { System.out.println("i:" + i); } } } public class StartThreadTest { public static void main(String[] args) { System.out.println("-----多线程创建开始-----"); // 1.创建一个线程 CreateThread createThread = new CreateThread(); // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法 System.out.println("-----多线程创建启动-----"); createThread.start(); System.out.println("-----多线程创建结束-----"); }
调用start方法后,代码并没有从上往下执行,而是有一条新的执行分之。注意:画图演示多线程不同执行路径。
第二种实现Runnable接口,重写run方法
class CreateRunnable implements Runnable { @Override public void run() { for (int i = 0; i< 10; i++) { System.out.println("i:" + i); } } } public class RunnableTest { public static void main(String[] args) { System.out.println("-----多线程创建开始-----"); // 1.创建一个线程 CreateRunnable createThread = new CreateRunnable(); // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法 System.out.println("-----多线程创建启动-----"); Thread thread = new Thread(createThread); thread.start(); System.out.println("-----多线程创建结束-----"); } }
第三种使用匿名内部类方式
1 System.out.println("-----多线程创建开始-----"); 2 Thread thread = new Thread(new Runnable() { 3 public void run() { 4 for (int i = 0; i< 10; i++) { 5 System.out.println("i:" + i); 6 } 7 } 8 }); 9 thread.start(); 10 System.out.println("-----多线程创建结束-----");
使用继承Thread类还是使用实现Runnable接口好?
使用实现实现Runnable接口好,原因实现了接口还可以继续继承,继承了类不能再继承。
启动线程是使用调用start方法还是run方法?
开始执行线程 注意 开启线程不是调用run方法,而是start方法,调用run只是使用实例调用方法。
守护线程
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止,守护线程当进程不存在或主线程停止,守护线程也会被停止。
使用setDaemon(true)方法设置为守护线程。
用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
新建状态
当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
就绪状态
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
运行状态
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
阻塞状态
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
死亡状态
有两个原因会导致线程死亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
join()
等待调用 join 方法的线程执行结束,才执行后面的代码
其调用一定要在 start 方法之后(看源码可知)
使用场景:当父线程需要等待子线程执行结束才执行后面内容或者需要某个子线程的执行结果会用到 join 方法
1 class JoinThread implements Runnable { 2 3 public void run() { 4 for (int i = 0; i < 100; i++) { 5 System.out.println(Thread.currentThread().getName() + "---i:" + i); 6 } 7 } 8 } 9 18 public class JoinThreadDemo { 19 20 public static void main(String[] args) { 21 JoinThread joinThread = new JoinThread(); 22 Thread t1 = new Thread(joinThread); 23 Thread t2 = new Thread(joinThread); 24 t1.start(); 25 t2.start(); 26 try { 27 //其他线程变为等待状态,等t1线程执行完成之后才能执行join方法。 28 t1.join(); 29 } catch (Exception e) { 30 31 } 32 for (int i = 0; i < 100; i++) { 33 System.out.println("main ---i:" + i); 34 } 35 } 36 37 }
sleep()
sleep()方法是线程类(Thread)的静态方法,让调用的线程进入指定时间睡眠状态,使得当前线程进入阻塞状态,告诉系统至少在指定时间内不需要线程调度器为该线程分配执行时间片,给执行机会给其他线程(实际上,调用sleep()方法时并不要求持有任何锁,即sleep()可在任何地方使用。),但是监控状态依然保持,到时后会自动恢复。
当线程处于上锁时,sleep()方法不会释放对象锁,即睡眠时也持有对象锁。只会让出CPU执行时间片,并不会释放同步资源锁。
sleep()休眠时间满后,该线程不一定会立即执行,这是因为其他线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
sleep()必须捕获异常,在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。
在没有锁的情况下,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。
wait()
wait()方法是Object类里的方法,当一个线程执行wait()方法时,它就进入到一个和该对象相关的等待池中(进入等待队列,也就是阻塞的一种,叫等待阻塞),同时释放对象锁,并让出CPU资源,待指定时间结束后返还得到对象锁。
wait()使用notify()方法、notiftAll()方法或者等待指定时间来唤醒当前等待池中的线程。等待的线程只是被激活,但是必须得再次获得锁才能继续往下执行,也就是说只要锁没被释放,原等待线程因为为获取锁仍然无法继续执行。notify的作用只负责唤醒线程,线程被唤醒后有权利重新参与线程的调度。
wait()方法、notify()方法和notiftAll()方法用于协调多线程对共享数据的存取,所以只能在同步方法或者同步块中使用,否则抛出IllegalMonitorStateException。
sleep和wait的区别
(1)属于不同的两个类,sleep()方法是线程类(Thread)的静态方法,wait()方法是Object类里的方法。
(2)sleep()方法不会释放锁,wait()方法释放对象锁。
(3)sleep()方法可以在任何地方使用,wait()方法则只能在同步方法或同步块中使用。
(4)sleep()必须捕获异常,wait()方法、notify()方法和notiftAll()方法不需要捕获异常。
(5)sleep()使线程进入阻塞状态(线程睡眠),wait()方法使线程进入等待队列(线程挂起),也就是阻塞类别不同。
Yield()
Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
该方法与sleep()类似,都是可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态(可运行状态)。
yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
join作用是让其他线程变为等待, t1.join();// 让其他线程变为等待,直到当前t1线程执行完毕,才释放。
valitate 关键字
定义
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
valitate是轻量级的synchronized,不会引起线程上下文的切换和调度,执行开销更小。
原理
1. 使用volitate修饰的变量在汇编阶段,会多出一条lock前缀指令
2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
3. 它会强制将对缓存的修改操作立即写入主存
4. 如果是写操作,它会导致其他CPU里缓存了该内存地址的数据无效
作用
内存可见性
多线程操作的时候,一个线程修改了一个变量的值 ,其他线程能立即看到修改后的值
防止重排序
即程序的执行顺序按照代码的顺序执行(处理器为了提高代码的执行效率可能会对代码进行重排序)
并不能保证操作的原子性(比如下面这段代码的执行结果一定不是100000
多线程分批处理数据
需求:目前蚂蚁课堂有10万个用户,现在蚂蚁课堂需要做活动,给每一个用户发送一条祝福短信。
为了提高数程序的效率,请使用多线程技术分批发送据。
每开一个线程,都会占用CPU资源
服务器(电脑)配置 CPU 核数
新建用户实体类
1 package com.itmayiedu.enity; 2 /** 3 * 4 */ 5 publicclass UserEntity { 6 private String userId; 7 private String userName; 8 public String getUserId() { 9 returnuserId; 10 } 11 publicvoid setUserId(String userId) { 12 this.userId = userId; 13 } 14 public String getUserName() { 15 return userName; 16 } 17 18 publicvoid setUserName(String userName) { 19 this.userName = userName; 20 } 21 }
建立多线程UserThread 执行发送短信
1 Class UserThreadextends Thread { 2 private List<UserEntity>list; 3 /** 4 * 通过构造函数 传入每个线程需要执行的发送短信内容 5 * 6 * @param list 7 */ 8 public UserThread(List<UserEntity>list) { 9 this.list = list; 10 } 11 /** 12 * 13 */ 14 publicvoid run() { 15 for (UserEntity userEntity : list) { 16 System.out.println("threadName:" + Thread.currentThread().getName() + "-学员编号:" + userEntity.getUserId() 17 + "---学员名称:" + userEntity.getUserName()); 18 // 调用发送短信具体代码 19 } 20 } 21 }
初始化数据
1 publicstatic List<UserEntity> init() { 2 List<UserEntity>list = new ArrayList<UserEntity>(); 3 for (inti = 1; i<= 140; i++) { 4 UserEntity userEntity = new UserEntity(); 5 userEntity.setUserId("userId" + i); 6 userEntity.setUserName("userName" + i); 7 list.add(userEntity); 8 } 9 returnlist; 10 11 }
计算分页工具类
public class ListUtils { /** * */ static public<T> List<List<T>> splitList(List<T> list, int pageSize) { int listSize = list.size(); int page = (listSize + (pageSize - 1)) / pageSize; List<List<T>>listArray = new ArrayList<List<T>>(); for (int i = 0; i<page; i++) { List<T>subList = new ArrayList<T>(); for (int j = 0; j<listSize; j++) { int pageIndex = ((j + 1) + (pageSize - 1)) / pageSize; if (pageIndex == (i + 1)) { subList.add(list.get(j)); } if ((j + 1) == ((j + 1) * pageSize)) { break; } } listArray.add(subList); } return listArray; } }
实现发送短信
1 Public staticvoid main(String[] args) { 2 // 1.初始化用户数据 3 List<UserEntity>listUserEntity = init(); 4 // 2.计算创建创建多少个线程并且每一个线程需要执行“分批发送短信用户” 5 // 每一个线程分批跑多少 6 intuserThreadPage = 50; 7 // 计算所有线程数 8 List<List<UserEntity>>splitUserList = ListUtils.splitList(listUserEntity, userThreadPage); 9 intthreadSaze = splitUserList.size(); 10 for (inti = 0; i<threadSaze; i++) { 11 List<UserEntity>list = splitUserList.get(i); 12 UserThread userThread = new UserThread(list); 13 // 3.执行任务发送短信 14 userThread.start(); 15 } 16 17 }
----总结来自蚂蚁课堂&每特学院材料
https://blog.csdn.net/qq_34490018/article/details/81609147