26届JAVA 学习日记——Day6

2024.11.07 周四
行程总结:上午睡觉、下午学习(写了点课程的作业)、晚上学习

知识总结:进程线程相关知识、锁相关知识、lambda(函数式接口)语法

八股

进程 & 线程

Java 底层会调用 pthread_create 来创建线程,所以本质上 java 程序创建的线程,就是和操作系统线程是一样的,是 1 对 1 的线程模型。

线程和进程的区别?

线程是调度的基本单位,而进程则是资源拥有的基本单位。

  • 当进程只有一个线程时,可以认为进程就等于线程;
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;
线程的创建方式有哪些?

1.继承Thread类
自定义类继承java.lang.Thread类,重写其run()方法,run()方法中定义了线程执行的具体任务。创建该类的实例后,通过调用start()方法启动线程。

  • 优点: 编写简单,如果需要访问当前线程,无需使用Thread.currentThread ()方法,直接使用this,即可获得当前线程
  • 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类
class MyThread extends Thread{
	@Overrie
	publci void run(){
		// 线程执行的代码
	}
}

public static void main(String[] args){
	MyThread t = new MyThread();
	t.start();
} 

2.实现Runnable接口
实现java.lang.Runnable接口。实现Runnable接口需要重写run()方法,然后将此Runnable对象作为参数传递给Thread类的构造器,创建Thread对象后调用其start()方法启动线程。

  • 优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
  • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
class MyRunnable implements Runnable {
	@Override
	public void run(){
		// 线程执行的代码
	}
}

public static void main(String[] args){
	Thread t = new Thread(new MyRunnable());
	t.start();
}

3.实现Callable接口与FutureTask
java.util.concurrent.Callable接口类似于Runnable,但Callable的call()方法可以有返回值并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接口

  • 缺点:编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
  • 优点:线程只是实现Runnable或实现Callable接口,还可以继承其他类。这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
class MyCallable implements Callable<Integer>{
	@Override
	public Integer call() throws Exception{
		// 线程执行的代码,这里要返回一个整形结果
		return 1;
	}
}

public static void main(String[] args){
	MyCallable task = new MyCallable();
	FutureTask<Integer> futureTask = new FutureTask<>(task);
	Thread t = new Thread(futureTask);
	t.start();
}  

4.使用线程池(Executor框架)
从Java 5开始引入的java.util.concurrent.ExecutorService和相关类提供了线程池的支持,这是一种更高效的线程管理方式,避免了频繁创建和销毁线程的开销。可以通过Executors类的静态方法创建不同类型的线程池。

  • 缺点:程池增加了程序的复杂度,特别是当涉及线程池参数调整和故障排查时。错误的配置可能导致死锁、资源耗尽等问题,这些问题的诊断和修复可能较为复杂。
  • 优点:线程池可以重用预先创建的线程避免了线程创建和销毁的开销,显著提高了程序的性能。对于需要快速响应的并发请求,线程池可以迅速提供线程来处理任务,减少等待时间。并且,线程池能够有效控制运行的线程数量,防止因创建过多线程导致的系统资源耗尽(如内存溢出)。通过合理配置线程池大小,可以最大化CPU利用率和系统吞吐
class Task implements Runnable{
	@Override
	public void run(){
		// 线程执行的代码
	}
}

public static void main(String[] args){
	// 创建固定大小的线程池
	ExecutorService executor = Executors.newFixedThreadPool(10);
	// 通过循环体不断将实现Runnable接口的task加入线程池
	for (int i = 0; i < 100; i++){
		executor.submit(new Task()); // 提交任务到线程池执行
	}
	executor.shutdown(); // 关闭线程池
}

互斥锁与自旋锁

加锁的目的就是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。

当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的

  • 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞

  • 互斥锁加锁失败后,线程会释放 CPU ,给其他线程(线程切换)。
    互斥锁

  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁。

自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。

一般加锁的过程,包含两个步骤:

  • 第一步,查看锁的状态,如果锁是空闲的,则执行第二步;
  • 第二步,将锁设置为当前线程持有;
读写锁

由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。

根据实现的不同,读写锁可以分为「读优先锁」和「写优先锁」。

  • 「读优先锁」期望的是,读锁能被更多的线程持有,以便提高读线程的并发性
    读优先锁

  • 「写优先锁」是优先服务写线程
    写优先锁
    读优先锁造成了写线程「饥饿」的现象:如果一直有读线程获取读锁,那么写线程将永远获取不到写锁。
    写优先锁造成了读线程「饥饿」的现象:但是如果一直有写线程获取写锁,读线程也会被「饿死」。

  • 「公平读写锁」:不偏袒任何一方。

公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。

乐观锁与悲观锁

悲观锁做事比较悲观,认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

  • 悲观锁是修改共享数据前,都要先加锁,防止竞争。

乐观锁做事比较乐观,假定冲突的概率很低,先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

  • 乐观锁是先修改同步资源,再验证有没有发生冲突。

在线文档可以同时多人编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。

服务端要怎么验证是否冲突的方案如下:

  • 由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号;
  • 当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号不一致则提交失败,如果版本号一致则修改成功,然后服务端版本号更新到最新的版本号。

算法

121.买卖股票的最佳时机(简单题)
169.多数元素(简单题)

项目

买了个项目(给报销的)准备不学苍穹外卖,学其他的直接准备简历啦

ThreadLocal

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

如何使用ThreadLocal?

以下两段代码等价

import java.text.SimpleDateFormat;
import java.util.Random;

// 定义ThreadLocalExample类实现Runnable接口
public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
     /**
      ThreadLocal类中提供一个静态方法withInitial()
      其接受一个Supplier函数式接口作为参数
      该Supplier在每次通过lambda表达式创建一个新的SimpleDateFormat对象
      这段代码确保每个线程都有自己独立的SimpleDateFormat实例
      */
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
    
    // ThreadLocalExample类中定义了一个main方法
    public static void main(String[] args) throws InterruptedException {
		// 在main中新建一个ThreadLocalExample类
        ThreadLocalExample obj = new ThreadLocalExample();
        /**
        	循环体内部,每次循环创建一个新的线程 t
        	将 obj 作为目标传递给线程
        	将循环变量 i 转换成字符串作为线程的名字
        */
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            // 线程在启动前休眠一个随机时间(0~999 ms)
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }
    
	// 重写Runnable接口的run()方法 
    @Override
    public void run() {
    	// 打印当前线程的name和默认的日期格式模式
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        // 将formatter的值设置成一个新的SimpleDateFormat实例,其使用默认的日期时间格式
        formatter.set(new SimpleDateFormat());
		// 再次打印当前线程的名称和修改后的日期格式模式。
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}
// 直接在ThreadLocal的实例化中重写其initialValue方法
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
	@Override
	protected SimpleDateFormat initialValue(){
		return new SimpleDateFormat("yyyyMMdd HHmm");
	}
}
ThreadLocal原理了解吗?

Thread类源代码入手

ThreadLocalMap 可以理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用ThreadLocal类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

public class Thread implements Runnable {
	// ......
	// 与此线程有关的ThreadLocal值,由ThreadLocal类维护
	ThreadLocal.ThreadLocalMap threadLocals = null;

	// 与此线程有关的InheritableThreadLocal值,由InheritableThreadLocal类维护
	ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

ThreadLocal类的set()方法

最终的变量是放在了当前线程的ThreadLocalMap中,并不是存在ThreadLocal上,ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key,Object对象为value的键值对。

public void set(T value) {
	// 获取当前请求的线程
	Thread t = Thread.currentThread();
	// 取出Thread类内部的threadLocals变量(哈希表结构)
	ThreadLocalMap map = getMap(t);
	if (map != null)
		// 将需要存储的值放入到这个哈希表中
		map.set(this, value);
	else
		createMap(t, value);
}

ThreadLocalMap getMap(Thread t){
	return t.threadLocals;
}
ThreadLocal内存泄露问题是怎么导致的?

ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强引用。所以如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。

因此,ThreadLocalMap中就会出现key为null的Entry。假如不做任何措施,value永远无法被GC回收,这个时候就可能会产生内存泄漏。ThreadLocalMap实现中已经考虑了该种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

弱引用介绍:

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它
所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,
因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java
虚拟机就会把这个弱引用加入到与之关联的引用队列中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值