Java基础复习-多线程
本文仅对学习过程中所缺java知识点的查缺补漏复习
多线程
- 程序(program):为了完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象;
- 进程(process):程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有自身的产生、存在和消亡的过程-生命周期;
- 程序是静态的,进程是动态的;
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域;
- 线程(thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的;
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小;
- 一个进程中的多个线程共享相同的内存单元/内存地址空间----->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就带来安全隐患;
一个Java应用程序java.exe
,至少有三个线程:main()
主线程、gc()
垃圾回收线程、异常处理线程。但是如果发生了异常,是会影响主线程的;
并行和并发
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
Java中多线程的使用
多线程的创建1:继承Thread类
方式1:继承于Thread
类
- 1.创建一个继承于
Thread
类的子类 - 2.重写
Thread
类的run()
方法:新线程要执行的操作 - 3.创建
Thread
类子类的对象 - 4.通过对象调用
start()
方法:启动当前新线程;JVM调用当前线程的run()
方法 - 注意:这里需要调用
start()
方法,不能直接调用run()
方法,调用run()
方法虽然不会报错,但是跟普通的对象调用方法就没区别了,就不会开启多线程。 - 如果需要再开启同一子类的第二个线程,需要重新
new
一个子类对象,不能用原来new
好的子类对象去再次调用start()
方法,不然会抛一个违法线程状态异常
public class MyThread extends Thread {
@Override
public void run() {
for (int i=0;i<100;i++)
if (i % 5 == 0)
System.out.println(i);
}
}
public class ThreadTest {
public static void main(String[] args){
MyThread myThread = new MyThread();
myThread.start();
for (int i=0;i<100;i++)
if (i % 5 != 0)
System.out.println("main:"+i);
}
}
Thread方法
Thread中的常用方法:
start()
:启动当前线程,调用当前线程的run()
;run()
:通常需要重写Thread
类中的此方法,将创建的线程要执行的操作声明在此方法中;currentThread()
:静态方法,返回执行当前代码的线程;etName()
:获取当前线程的名字;setName()
:设置当前线程的名字;yield()
:释放当前cpu的执行权,但是执行权可能下一刻还是轮到当前线程;join()
:在线程a中调用线程b的join()
,此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态;sleep(long millis)
:单位毫秒,令当前活动线程在指定时间段内放弃对CPU控制,使其它线程有机会被执行,时间到后重排队;isAlive()
:判断当前线程是否还活着。
线程优先级
MAX_PRIORITY:10
(最高线程优先级)MIN_PRIORITY:1
(最低线程优先级)NORM_PRIORITY:5
(默认线程优先级)getPriority()
:获取当前线程优先级;setPriority(int p)
:设置当前线程优先级- 优先级的设置在线程启动之前;
- 优先级高只是比较高概率会被执行,但不是高优先级的执行完才会执行低优先级的;
例子
三个窗口卖票1
/**
* 虽然下面这种方式可以模拟三个窗口卖100张票的场景,
* 但是可能会出现并发,线程不安全,后面将会改进,
* 这里的代码旨在模拟多线程
**/
public class Window extends Thread {
private static int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0)
System.out.println(getName() + ":" + ticket--);
else
break;
}
}
public static void main(String[] args){
Window window1 = new Window();
Window window2 = new Window();
Window window3 = new Window();
window1.setName("线程1");
window2.setName("线程2");
window3.setName("线程3");
window1.start();
window2.start();
window3.start();
}
}
多线程的创建2:实现Runnable接口
- 创建一个实现了
Runnable
接口的类; - 实现类去实现
Runnable
中的抽象方法:run()
; - 创建实现类的对象;
- 将此对象作为参数传递到
Thread
类的构造器中,创建Thread
类的对象; - 通过
Thread
类的对象调用start()
。
public class MRunnable implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++)
if(i % 2 == 0)
System.out.println(Thread.currentThread().getName()+":"+i);
}
public static void main(String[] args){
MRunnable mRunnable = new MRunnable();
Thread thread1 = new Thread(mRunnable);
thread1.setName("线程1");
Thread thread2 = new Thread(mRunnable);
thread2.setName("线程2");
thread1.start();
thread2.start();
}
}
三个窗口卖票2
/**
* 跟卖票1不一样的是,这里的ticket票数没有使用static,
* 因为上面是new了3个对象,所以有3个ticket
* 但这里是只new了一个对象,给了3个线程用
**/
public class MRunnable1 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true) {
if (ticket > 0)
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
else
break;
}
}
public static void main(String[] args){
MRunnable1 mRunnable1 = new MRunnable1();
Thread thread1 = new Thread(mRunnable1);
Thread thread2 = new Thread(mRunnable1);
Thread thread3 = new Thread(mRunnable1);
thread1.setName("线程1");
thread2.setName("线程2");
thread3.setName("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
开发中选择
优先使用实现Runnable
方式:
- 实现的方式没有类单继承性的局限性;
- 实现的方式更适合来处理多个线程有共享数据的情况。
联系:public class Thread implements Runnable
相同点:两种方式都需要重写run()
,将线程要执行的逻辑声明在run()
中。
线程的生命周期
JDK中用Thread.State
类定义了线程的几种状态
线程的一个完整生命周期可能存在五个的状态:
- 新建:当一个
Thread
类或其子类的对象被声明并创建时(即new出来),新生的线程对象处于新建状态; - 就绪:处于新建状态的线程被
start()
后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源; - 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,
run()
方法定义了线程的操作和功能; - 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束;
线程同步
在Java中,通过同步机制来解决线程的安全问题:
方式一:同步代码块
//共享数据:多个线程共同操作的变量
//锁:任何一个类的对象,都可以充当锁
//锁要求:多个线程必须要共用一把锁
//好处:解决了线程安全问题;
//局限性:操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低。
synchronized(同步监视器:锁){
//需要被同步的代码:即操作共享数据的代码
}
例子
public class MRunnable1 implements Runnable {
private int ticket = 100;
//锁:任何类的对象
Object obj = new Object();
@Override
public void run() {
while(true) {
synchronized(obj) {
// synchronized(this){ this是指当前对象,因为实现Runnable方式只new了一个对象,所以可以当锁,但如果是继承线程的方式,那就不行,因为有多个锁
// synchronized(MRunnable1.class){ 可以拿类当锁,类也是对象,因为类只加载一次
if (ticket > 0)
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
else
break;
}
}
}
public static void main(String[] args){
MRunnable1 mRunnable1 = new MRunnable1();
Thread thread1 = new Thread(mRunnable1);
Thread thread2 = new Thread(mRunnable1);
Thread thread3 = new Thread(mRunnable1);
thread1.setName("线程1");
thread2.setName("线程2");
thread3.setName("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
方式二:同步方法
如果操作共享数据的代码完整地声明在一个方法中,那么我们不妨将此方法声明为同步的。
总结:
- 同步方法仍然使用到了同步监视器,只是不用显式声明;
- 非静态的同步方法,锁:this;
- 静态的同步方法,锁:当前类本身(xxx.class);
例子
public class MRunnable2 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
//同步方法
private synchronized void show(){ //锁是this
if (ticket > 0)
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
}
public static void main(String[] args){
MRunnable2 mRunnable1 = new MRunnable2();
Thread thread1 = new Thread(mRunnable1);
Thread thread2 = new Thread(mRunnable1);
Thread thread3 = new Thread(mRunnable1);
thread1.setName("线程1");
thread2.setName("线程2");
thread3.setName("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
改写懒汉式
/**
* 改写懒汉式
* @Author: fxx
* @Date: 2020/12/22 13:27
*/
public class Bank {
private Bank(){}
private static Bank instance = null;
// public synchronized static Bank getInstance(){
// if (instance == null)
// instance = new Bank();
// return instance;
// }
//这种方式等同于上面那种同步方法的方式,但是效率不高
// public static Bank getInstance(){
// synchronized(Bank.class) {
// if (instance == null)
// instance = new Bank();
// return instance;
// }
// }
//相对于上面两种方式来说,效率提高了,
// 因为只有还没创建的时候会导致同步操作,其他时间都不会出现同步操作
public static Bank getInstance(){
if(instance == null){
synchronized(Bank.class) {
if (instance == null)
instance = new Bank();
}
}
return instance;
}
}
线程死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法
- 专门的算法,原则;
- 尽量减少同步资源的定义;
- 尽量避免嵌套同步。
方式三:Lock锁
JDK5.0新增
/**
* lock方式
* @Author: fxx
* @Date: 2020/12/19 20:47
*/
public class MRunnable3 implements Runnable {
private int ticket = 100;
//Lock锁的一种实现,默认不公平方式,即不会平均分配到每个线程,
// 可以通过构造参数设置为true,改为公平方式
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try{
lock.lock(); //调用加锁方法
if (ticket > 0)
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
else
break;
}finally {
lock.unlock(); //调用解锁方法:手动解锁,一定要解锁
}
}
}
public static void main(String[] args){
MRunnable3 mRunnable1 = new MRunnable3();
Thread thread1 = new Thread(mRunnable1);
Thread thread2 = new Thread(mRunnable1);
Thread thread3 = new Thread(mRunnable1);
thread1.setName("线程1");
thread2.setName("线程2");
thread3.setName("线程3");
thread1.start();
thread2.start();
thread3.start();
}
}
进程通信
三个方法:
wait()
:一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器;notify()
:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,那么唤醒优先级高的那个;notifyAll()
:一旦执行此方法,就会唤醒被wait的所有线程。
说明:
wait(),notify(),notifyAll()
三个方法必须使用在同步代码块或同步方法中;wait(),notify(),notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会因为锁不一致出现IllegalMonitorStateException
异常;wait(),notify(),notifyAll()
三个方法是定义在java.lang.Object
类中的。
/**
* 线程通信:让两个线程,交互打印1-100之间的数
* @Author: fxx
* @Date: 2020/12/22 14:43
*/
public class Communication {
public static void main(String[] args){
Number number = new Number();
Thread thread1 = new Thread(number);
Thread thread2 = new Thread(number);
thread1.setName("线程1");
thread2.setName("线程2");
thread1.start();
thread2.start();
}
}
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while(true){
synchronized (this){
notify();
if(number <= 100){
System.out.println(Thread.currentThread().getName() + ":"+number++);
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
生产者和消费者
/**
* 生产者---消费者问题
* 一家面包店,生产的面包不能超过20个
* @Author: fxx
* @Date: 2020/12/22 15:17
*/
/**
* 消费者
*/
class Customer implements Runnable{
private Clerk clerk;
public Customer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.custom();
}
}
}
/**
* 生产者
*/
class Producer implements Runnable{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produce();
}
}
}
/**
* 店铺
*/
class Clerk{
private int bread = 0; //面包总数
//生产面包
public synchronized void produce(){
if(bread < 20){
bread++;
System.out.println(Thread.currentThread().getName() + "生产第" + bread + "面包");
notify(); //已经生产了,唤醒消费者
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费面包
public synchronized void custom(){
if(bread > 0){
System.out.println(Thread.currentThread().getName() + "消费第" + bread + "面包");
bread--;
notify(); //已经消费了,唤醒生产者
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Production {
public static void main(String[] args){
Clerk clerk = new Clerk(); //new一家面包店
//再造几个人
//生产者
Producer producer = new Producer(clerk);
//消费者1和2
Customer customer1 = new Customer(clerk);
Customer customer2 = new Customer(clerk);
Thread thread1 = new Thread(producer);
Thread thread2 = new Thread(customer1);
Thread thread3 = new Thread(customer2);
thread1.setName("生产者1");
thread2.setName("消费者1");
thread3.setName("消费者2");
thread1.start();
thread2.start();
thread3.start();
}
}
JDK5.0新增线程创建方式
多线程的创建3:实现Callable接口
与Runnable
相比,Callable
功能更强大些
- 相比
run()
方法,可以有返回值; - 方法可以抛出异常;
- 支持泛型的返回值;
- 需要借助
Future Task
类,比如获取返回结果。
具体步骤
- 实现
Callable
接口 - 重写
call()
方法,相当于run()
方法,不过有返回值,可用于线程间通信 - new出实现了
Callable
接口的对象 new FutureTask
,并把上面new出来的对象传进去- 开启一个线程
- 如果要获取返回值,那么可以用
get()
方法,这里拿到的返回值就是call()
方法里的返回值
/**
* 线程创建方式之:实现Callable接口方式
* @Author: fxx
* @Date: 2020/12/22 16:11
*/
//1.实现Callable接口
class NumThread implements Callable{
//2.重写call方法,相当于run方法,不过有返回值,可用于线程间通信
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
System.out.println(i);
sum += i;
}
return sum;
}
}
public class MyCallable {
public static void main(String[] args){
//3.new出实现了Callable接口的对象
NumThread numThread = new NumThread();
//4.new FutureTask,并把上面new出来的对象传进去
FutureTask futureTask = new FutureTask(numThread);
//5.开启一个线程
new Thread(futureTask).start();
try {
//6.如果要获取返回值,那么可以用get()方法,这里拿到的返回值就是call()方法里的返回值
Object sum = futureTask.get();
System.out.println("综合:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
多线程创建4:线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完返回线程池。可以避免频繁创建销毁、实现重复利用。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理:设置线程池大小,最大线程数等
/**
* @Author: fxx
* @Date: 2020/12/22 17:03
*/
class NumExecutor1 implements Runnable{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i % 2 == 0)
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
class NumExecutor2 implements Runnable{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i % 2 != 0)
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
public class MyThreadPool{
public static void main(String[] args){
//1.提供指定数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
System.out.println(service.getClass()); //得到是哪个类造出了这个对象
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
//设置线程池属性
// service1.setCorePoolSize();等属性设置
//2.执行指定线程操作
//execute适用于Runnable接口的实现类
service.execute(new NumExecutor1());
service.execute(new NumExecutor2());
//submit适用于Callable接口的实现类
// service.submit();
//关闭线程池
service.shutdown();
}
}
释放锁操作
- 当前线程的同步方法、同步代码块执行结束;
- 当前线程在同步代码块、同步方法中遇到
break、return
终止了该代码块、该方法的继续执行; - 当前线程在同步代码块、同步方法中出现了未处理的
Error
或Exception
,导致异常结束; - 当前线程在同步代码块、同步方法中执行了线程对象的
wait()
方法,当前线程暂停,并释放锁。
不会释放锁操作
- 线程执行同步代码块或同步方法时,程序调用
Thread.sleep()、Thread.yield()
方法暂停当前线程的执行; - 线程执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程挂起,该线程不会释放锁(同步监视器)。- 应尽量避免使用
suspend()
和resume()
来控制线程。
- 应尽量避免使用
面试题
synchronized和Lock对比
- 两者都可以解决线程安全问题;
Lock
是显式锁(手动加锁和解锁),synchronized
是隐式锁,出了作用域自动释放;Lock
只有代码块加锁,synchronized
有代码锁和方法锁;- 使用
Lock
锁,JVM将花费较少的时间调度线程,性能更好。并且具有更好的拓展性(提供更多子类)。
优先使用顺序:
Lock
----->同步代码块(已经进入了方法体,分配了相应资源)----->同步方法(还在方法体外)
sleep()和wait()异同
-
相同点:
- 一旦执行方法,都可以使得当前的线程进入阻塞状态;
-
不同点:
- 两个方法声明的位置不同:Thread类中声明了
sleep()
,Object类中声明了wait()
- 调用的要求不同:
sleep()
可以在任何需要的场景下调用;但wait()
只能在同步代码块或同步方法中使用; - 如果两个方法都使用在同步代码块或同步方法中,
sleep()
的执行不会释放锁,但wait()
的执行会释放锁; sleep()
会自动唤醒,wait()
需要用notify()
或notifyAll()
唤醒;
- 两个方法声明的位置不同:Thread类中声明了