Week 2

发布时间 2023-09-24 17:09:09作者: 鹏懿如斯

week 2

基础知识部分看完了

基础知识

序列化

序列化破坏单例模式

序列化会破坏单例模式,因为在序列化过程中,每个已经序列化的对象都会添加一个特殊的标记,然后在反序列化时,如果遇到已序列化的对象,就不再序列化它,而是直接使用之前保存的标记。这样,反序列化后得到的对象就不是原来的单例对象了。

解决序列化破坏单例模式的方法有很多,例如,使用静态内部类等方式来实现单例模式,或者在饿汉式单例模式中添加一个readResolve目标方法。

  • 方法一
public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

在这个例子中,Singleton类只有一个私有构造函数,保证了外部无法通过new关键字创建该类的实例。SingletonHolder是一个静态内部类,它持有了一个Singleton类的实例,并且通过私有构造函数保证该实例的唯一性。getInstance()方法返回SingletonHolder中的实例,保证了单例模式的正确性。

需要注意的是,在使用静态内部类实现单例模式时,由于静态内部类和外部类共享同一个类加载器,因此需要注意线程安全问题。可以通过将静态内部类声明为final来避免这个问题。

  • 方法二
import java.io.*;

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    protected Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

在这个例子中,Singleton类只有一个私有构造函数,保证了外部无法通过new关键字创建该类的实例。readResolve()方法被重写,当对象被序列化时,JVM会自动调用该方法,将反序列化后的对象强制转换为Singleton类型。

需要注意的是,readResolve()方法需要抛出ObjectStreamException异常,这是因为反序列化过程中可能会发生各种错误,例如类未找到、IO异常等,需要将这些异常统一处理。

protobuf

Java序列化和protobuf是两种不同的序列化方式,它们之间没有直接的转换关系。

Java序列化是一种将对象转换为字节流的过程,可以用于在网络上传输数据或者将对象持久化到磁盘中。Java序列化的实现需要实现Serializable接口,并使用ObjectOutputStream类进行序列化和反序列化操作。

Protobuf是一种高效的二进制序列化协议,可以用于数据存储、通信协议等方面。它通过定义消息格式和字段类型来描述数据结构,可以自动生成代码,支持多种编程语言和平台。

虽然Java序列化和protobuf是两种不同的序列化方式,但是可以通过一些工具将Java对象转换为protobuf格式的数据,例如Google提供的Protocol Buffers Java API。这个API提供了一组类和方法,可以将Java对象转换为protobuf格式的数据,并且可以将protobuf格式的数据转换为Java对象。

泛型

类型擦除

Java 类型擦除是指在运行时,泛型类型信息会被擦除,只保留原始类型。这是因为 Java 泛型是在编译时实现的,而运行时的类型信息是不确定的。因此,在运行时,泛型类型会被替换为它们的原始类型,例如 Integer 被替换为 int,Double 被替换为 double 等。这种擦除机制使得 Java 代码可以在运行时保持与原始类型兼容,同时也增加了代码的安全性和可读性。

T K V E

  • T 是 Java 泛型中的一个类型参数,表示一个具体的类型。它可以用于定义一个泛型类或泛型接口,其中的成员变量可以是该类型的对象。

例如,我们可以定义一个泛型类 Box,它有一个成员变量 item,类型为 T:

public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

在上面的示例中,我们定义了一个 Box 类,它有一个成员变量 item,类型为 T。在构造函数中,我们传入了 T 的具体类型,并初始化了对应的成员变量。在 setItem 方法中,我们传入了 T 类型的参数,并将其赋值给成员变量 item;在 getItem 方法中,我们返回了成员变量 item 的值,其类型为 T。

由于 T 是一个类型参数,因此我们可以传入任意类型的对象作为 Box 类的实例。例如,我们可以创建一个 Box 类的实例,其中的元素类型为 Integer:

Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
Integer item = integerBox.getItem(); // item 的类型为 Integer

在上面的示例中,我们创建了一个 Box 类的实例,其中的元素类型为 Integer。在 setItem 方法中,我们传入了一个 Integer 类型的参数,并将其添加到成员变量 item 中;在 getItem 方法中,我们返回了成员变量 item 中的最后一个元素,其类型为 Integer。

  • K V 一般用于 key value
  • E 一般用于集合的元素 element

限定通配符

Java 泛型中有两种通配符:限定通配符和非限定通配符。

  1. 限定通配符:用在泛型类或泛型接口的边界上,表示该类型必须是某个具体的类型。例如,List<?> 表示一个元素类型未知的列表,但必须是一个列表。
  2. 非限定通配符:用在泛型类或泛型接口的边界上,表示该类型可以是任意类型。例如,List
// 限定通配符
public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

// 非限定通配符
public class Box {
    private Object item;

    public void setItem(Object item) {
        this.item = item;
    }

    public Object getItem() {
        return item;
    }
}

在上面的示例中,Box 类的第一个版本使用了限定通配符 T,表示元素类型必须是某个具体的类型。第二个版本则使用了非限定通配符 Object,表示元素类型可以是任意类型。

上下界限定符

Java 泛型中的上下界限定符 extends 和 super 用于限制泛型类型参数的上界或下界。

extends:表示泛型类型参数必须是指定类型的子类或实现指定接口。例如,List<? extends Number> 表示一个元素类型为 Number 或其子类的列表。

super:表示泛型类型参数必须是指定类型的父类。例如,List<? super Integer> 表示一个元素类型为 Integer 或其父类的列表。

// 使用 extends 限制泛型类型参数的上界
public class Box<T extends Number> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

// 使用 super 限制泛型类型参数的下界
public class Box<T super Integer> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

在上面的示例中,Box 类的第一个版本使用了 extends 限制了泛型类型参数的上界,表示元素类型必须是 Number 或其子类。第二个版本则使用了 super 限制了泛型类型参数的下界,表示元素类型必须是 Integer 或其父类。

单元测试

JUnit 5 集成 SpringBoot

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyServiceTest {

    @Autowired
    private MyService myService;

    @MockBean
    private MyRepository myRepository;

    @Test
    public void testMyService() {
        // Arrange
        String expectedResult = "Hello, World!";
        when(myRepository.getData()).thenReturn(expectedResult);

        // Act
        String actualResult = myService.doSomething();

        // Assert
        assertEquals(expectedResult, actualResult);
    }
}

在上面的示例中,我们使用了@RunWith(SpringRunner.class)注解来指定使用SpringRunner运行测试类。@SpringBootTest注解用于启动Spring应用程序上下文并加载所有相关的Bean。@Autowired注解用于自动注入需要测试的服务或组件。@MockBean注解用于模拟一个依赖项,以便在测试期间对其进行测试。

在testMyService()方法中,我们首先使用when()方法来定义当myRepository.getData()返回什么值时,应该调用myService.doSomething()方法。然后,我们使用assertEquals()方法来验证实际结果是否与预期结果相同。

mock

Mock是一种模拟真实对象的行为的技术,通常用于单元测试中。在单元测试中,我们通常需要模拟一些外部依赖项,例如数据库、网络连接等。使用Mock可以避免这些依赖项对单元测试的影响,并提高测试的可靠性和可重复性。

JUnit提供了Mockito框架来支持Mock。Mockito是一个流行的Java Mock框架,它提供了一组简单易用的API来创建和配置Mock对象。

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;

public class MyServiceTest {

    @Test
    public void testMyService() {
        // 创建Mock对象
        MyRepository myRepository = Mockito.mock(MyRepository.class);

        // 配置Mock对象的行为
        when(myRepository.getData()).thenReturn("Hello, World!");

        // 创建被测试的服务对象,并将Mock对象注入其中
        MyService myService = new MyService(myRepository);

        // 调用被测试的方法
        String result = myService.doSomething();

        // 验证结果是否符合预期
        assertEquals("Hello, World!", result);

        // 验证是否调用了Mock对象的方法
        verify(myRepository, times(1)).getData();
    }
}

在上面的示例中,我们首先使用Mockito.mock()方法创建了一个Mock对象myRepository。然后,我们使用when()方法来定义当myRepository.getData()返回什么值时,应该调用myService.doSomething()方法。接下来,我们创建了被测试的服务对象myService,并将Mock对象注入其中。最后,我们调用被测试的方法,并使用assertEquals()方法验证结果是否符合预期。我们还使用verify()方法验证是否调用了Mock对象的方法。

内存数据库 h2

H2是一个使用Java实现的内存数据库,支持标准的SQL语法,支持大部分的MySQL语法和函数,很适合依赖关系型数据库(比如MySQL, SQL Server, Oracle等)的单元测试。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.Mockito.*;

@SpringBootTest
public class MyServiceTest {

    @Autowired
    private MyService myService;

    @MockBean
    private MyRepository myRepository;

    @Test
    public void testMyService() {
        // Arrange
        String expectedResult = "Hello, World!";
        when(myRepository.getData()).thenReturn(expectedResult);

        // Act
        String actualResult = myService.doSomething();

        // Assert
        assertEquals(expectedResult, actualResult);
    }
}

在上面的示例中,我们使用了@SpringBootTest注解来启动Spring应用程序上下文并加载所有相关的Bean。我们还使用了@MockBean注解来模拟一个依赖项,以便在测试期间对其进行测试。在testMyService()方法中,我们首先使用when()方法来定义当myRepository.getData()返回什么值时,应该调用myService.doSomething()方法。然后,我们使用assertEquals()方法来验证实际结果是否与预期结果相同。

常见的Java工具库

google guava

Google Guava 是 Java 通用库的开源集合,主要由 Google 工程师开发。它包括许多实用的工具,如集合类型、不可变集合、图库,以及用于并发、I/O、Hash、缓存、字符串等的实用工具。

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.base.Predicates;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class GuavaExample {
    public static void main(String[] args) {
        // 创建一个 ArrayList
        ArrayList<String> arrayList = Lists.newArrayList();

        // 创建一个 LinkedList
        LinkedList<String> linkedList = Lists.newLinkedList();

        // 创建一个 HashSet
        Set<String> hashSet = Sets.newHashSet();

        // 创建一个 HashMap
        Map<String, Integer> hashMap = Maps.newHashMap();

        // 判断两个对象是否相等
        boolean isEqual = Predicates.equalTo("apple", "apple");
        System.out.println("isEqual: " + isEqual); // true

        // 返回一个始终为真的谓词
        boolean alwaysTrue = Predicates.alwaysTrue();
        System.out.println("alwaysTrue: " + alwaysTrue); // true

        // 返回一个新的谓词,当且仅当当前谓词和另一个谓词都为真时才为真
        Predicates<String> andPredicate = Predicates.and(Predicates.equalTo("apple"), Predicates.equalTo("banana"));
        System.out.println("andPredicate: " + andPredicate.apply("apple")); // true
        System.out.println("andPredicate: " + andPredicate.apply("orange")); // false

        // 返回一个新的谓词,当当前谓词为假时为真
        Predicates<String> notPredicate = Predicates.not(Predicates.equalTo("apple"));
        System.out.println("notPredicate: " + notPredicate.apply("apple")); // false
        System.out.println("notPredicate: " + notPredicate.apply("banana")); // true
    }
}

hutool

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.poi.excel.ExcelUtil;
import org.dom4j.Document;
import org.dom4j.Element;

import java.io.File;
import java.util.Date;

public class HutoolExample {
    public static void main(String[] args) {
        // 将字符串写入文件
        FileUtil.writeStringToFile("Hello World!", "test.txt");

        // 格式化日期
        Date date = new Date();
        System.out.println(DateUtil.format(date, "yyyy-MM-dd HH:mm:ss"));

        // 判断字符串是否为空
        System.out.println(StrUtil.isEmpty("")); // true
        System.out.println(StrUtil.isEmpty("Hello World!")); // false

        // 向数组中添加元素
        Integer[] arr = new Integer[]{1, 2, 3};
        ArrayUtil.add(arr, 4);
        System.out.println(ArrayUtil.toString(arr)); // [1, 2, 3, 4]

        // 将对象转换为 JSON 字符串
        Object obj = new Person("Tom", 20);
        String jsonStr = JSONUtil.toJsonStr(obj);
        System.out.println(jsonStr); // {"name":"Tom","age":20}

        // 将对象转换为 XML 字符串
        Document doc = DocumentHelper.createDocument();
        Element root = doc.addElement("person");
        root.addAttribute("name", "Tom");
        root.addAttribute("age", 20);
        doc.asXML();
        System.out.println(doc.asXML()); // <person name="Tom" age="20"/>
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // getter & setter
}

时间处理

时区转换

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

/**
 * {@code @Author:} weiyupeng
 * {@code @Date:} 2023/9/21 20:13
 */
public class Main {
    public static void main(String[] args) throws Exception {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        formatter.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); // 设置源时区
        Date sourceDate = new Date();
        String sourceDateStr = formatter.format(sourceDate);
        System.out.println(sourceDateStr); // 2023-09-21 20:55:00

        formatter.setTimeZone(TimeZone.getTimeZone("America/New_York")); // 设置目标时区
        String targetDate = formatter.format(sourceDate);
        System.out.println(targetDate); // 2023-09-21 08:55:00
    }
}

SimpleDateFormat 线程安全问题

SimpleDateFormat是线程不安全的,因为它在多线程环境下会对共享资源进行操作,而这些操作不是原子性的,可能会导致数据不一致的问题。例如,如果两个线程同时调用SimpleDateFormat的parse方法,其中一个线程可能会修改SimpleDateFormat的内部状态,从而导致另一个线程解析出来的日期不正确。

为了避免这种情况,可以使用ThreadLocal来创建一个线程安全的SimpleDateFormat实例。这样,每个线程都会有自己的SimpleDateFormat实例,从而避免了多线程安全问题 。

import javax.annotation.Nullable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * {@code @Author:} weiyupeng
 * {@code @Date:} 2023/9/21 21:11
 */
public class DateFormatUtil {

//    private static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
//        @Override
//        protected SimpleDateFormat initialValue() {
//            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        }
//    };
    private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );

    public static String format(@Nullable String pattern, Date date) {
        SimpleDateFormat sdf = dateFormat.get();
        if (pattern != null) {
            sdf.applyPattern(pattern);
        }
        synchronized (sdf) {
            return sdf.format(date);
        }
    }

    public static Date parse(@Nullable String pattern, String dateStr) throws ParseException {
        SimpleDateFormat sdf = dateFormat.get();
        if (pattern != null) {
            sdf.applyPattern(pattern);
        }
        synchronized (sdf) {
            return sdf.parse(dateStr);
        }
    }

    public static void main(String[] args) throws ParseException {
        String format = format("yyyy-MM-dd HH:mm:ss.SSS", new Date());
        System.out.println(format);
        Date parse = parse("yyyy-MM-dd", "2023-09-21");
        System.out.println(parse.getTime());
    }
}

yyyy 和 YYYY 区别

“Y”,表示Week year。Week year意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算入下一年;而“y”则表示现在的所在年份。

编码方式

ASCII

ASCII 由美国国家标准协会(ANSI)于1963年发布,它包括了大小写字母、数字、标点符号以及一些控制字符。每个字符都用一个8位的二进制数(无符号数)表示,其中0-31分别表示字符在ASCII表中的位置,如大写字母A的十进制编码是65,转换为ASCII码就是0100 0001。

Unicode

Unicode是一种字符集,它规定了每个字符的二进制编码,以便在不同的计算机和操作系统之间交换文本。UTF-8、UTF-16和UTF-32都是Unicode的实现方式之一,它们将Unicode标准中的每个字符编码为不同的字节长度,以便在网络传输和存储中使用。

UTF-8是一种变长的编码方式,可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-16使用2个或4个字节表示一个符号,而UTF-32使用4个字节表示一个符号。这些编码方式都是为了在内存中存储字符而对Unicode字符编号进行编码 。

UTF-8使用1~4字节为每个字符编码:

  1. 一个US-ASCII字符只需1字节编码(Unicode范围由U+0000~U+007F)。
  2. 带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文等字母则需要2字节编码(Unicode范围由U+0080~U+07FF)。
  3. 其他语言的字符(包括中日韩文字、东南亚文字、中东文字等)包含了大部分常用字,使用3字节编码。

GB

GB2312(GB0)< GBK < GB18030

URL

URL编解码是一种将URL中的特殊字符转换为特定格式的过程,以便更易于读取和理解,同时避免与URL中的特殊字符冲突,使用%进行编码。

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * {@code @Author:} weiyupeng
 * {@code @Date:} 2023/9/23 12:40
 */
public class URLEncodingExample {
    public static void main(String[] args) throws UnsupportedEncodingException {
        String originalURL = "http://example.com/search?q=Web+Development&page=1&sort=asc";
        String encodedURL = URLEncoder.encode(originalURL, StandardCharsets.UTF_8.toString());
        System.out.println("Encoded URL: " + encodedURL);

        String decodedURL = URLDecoder.decode(encodedURL, StandardCharsets.UTF_8.toString());
        System.out.println("Decoded URL: " + decodedURL);
    }
}

输出:

Encoded URL: http%3A%2F%2Fexample.com%2Fsearch%3Fq%3DWeb%2BDevelopment%26page%3D1%26sort%3Dasc
Decoded URL: http://example.com/search?q=Web+Development&page=1&sort=asc

Big Endian 和 Little Endian

Big Endian 和 Little Endian 是指在多字节数据(如二进制文件、网络传输的数据包等)中,字节序的排列方式。

  • Big Endian:将字节序列中的高位字节排在前面,低位字节排在后面。例如一个32位的整数0x12345678,它的Big Endian排列方式就是0x12 0x34 0x56 0x78。
  • Little Endian:将字节序列中的低位字节排在前面,高位字节排在后面。例如同样的一个32位的整数0x12345678,它的Little Endian排列方式就是0x78 0x56 0x34 0x12。

在计算机系统中,不同的处理器架构可能采用不同的字节序方式,如Intel x86系列处理器采用的是Little Endian字节序,而Power PC、ARM、MIPS等处理器则采用Big Endian字节序。在进行跨平台数据传输时需要考虑到字节序的问题,以确保数据的一致性和正确性。

并发编程

线程池

Java线程池的设计需要考虑以下几个方面:

  1. 线程池大小:线程池的大小应该根据系统负载和硬件资源来设置,一般来说,线程池大小应该等于CPU核心数。
  2. 任务队列:线程池中的任务队列用于存放待执行的任务,可以使用阻塞队列或优先级队列等数据结构来实现。
  3. 线程工厂:线程工厂用于创建新的线程,可以使用ThreadPoolExecutor的构造函数中的ThreadFactory参数来指定线程工厂。
  4. 拒绝策略:当线程池已经达到最大容量时,新提交的任务应该如何处理?可以选择抛出异常或者将任务放入等待队列中等待执行。
  5. 线程池关闭方式:线程池在使用完毕后需要关闭,可以使用shutdown()方法来关闭线程池,该方法会等待所有已提交的任务执行完毕再关闭线程池。
  6. 监控和管理:可以使用一些第三方库来监控和管理线程池,比如JConsole、VisualVM等。这些工具可以提供线程池的运行状态、性能指标等信息,帮助我们更好地优化线程池的性能。
import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.AbortPolicy());

        // 提交任务
        for (int i = 0; i < 20; i++) {
            final int index = i;
            threadPool.execute(() -> {
                System.out.println("Task " + index + " is running by " + Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        threadPool.shutdown();
    }
}

以上代码中,我们创建了一个大小为5的线程池,其中核心线程数为5,最大线程数为10,空闲线程存活时间为200毫秒,任务队列使用ArrayBlockingQueue实现,容量为100。当线程池已满时,新提交的任务将被放入等待队列中等待执行,如果等待队列也满了,则采用AbortPolicy策略抛出RejectedExecutionException异常。最后,我们在所有任务执行完毕后关闭了线程池。

corePoolSize(核心线程数)

Java线程池中的corePoolSize参数表示线程池中的核心线程数,即线程池中始终存在的线程数量。当有任务提交时,如果当前线程数小于corePoolSize,则直接创建新线程执行任务;如果当前线程数已经达到corePoolSize,则将任务放入等待队列中等待执行。

使用经验:

  1. 核心线程数应该根据系统负载和硬件资源来设置,一般来说,应该等于CPU核心数。
  2. 如果corePoolSize设置过小,会导致任务经常处于等待状态,从而影响系统的性能表现。
  3. 如果corePoolSize设置过大,会导致系统频繁地创建和销毁线程,增加系统的开销,降低系统的性能表现。
  4. 在实际应用中,可以根据需要动态调整corePoolSize的大小,以适应不同的业务场景和负载情况。例如,在业务低峰期可以将corePoolSize设置得小一些,而在业务高峰期可以将corePoolSize设置得大一些。

maximumPoolSize(最大线程数)

Java线程池的maximumPoolSize参数表示线程池中最大的线程数,当所有核心线程都在忙碌时,如果有新的任务提交,线程池会创建新的线程来处理任务,直到线程数达到最大线程数为止。

使用maximumPoolSize需要注意以下几点:

  1. 应该根据系统负载和硬件资源来合理设置maximumPoolSize的值,一般来说,应该略大于CPU核心数。
  2. 如果maximumPoolSize设置得过大,会导致线程池中的线程过多,增加系统的开销和资源的浪费;如果设置得过小,会导致任务无法及时得到处理,影响系统的性能。
  3. 在使用线程池时,应该尽量避免创建过多的线程,因为每个线程都会占用一定的系统资源,过多的线程会导致系统资源的耗尽。
  4. 如果需要控制线程的数量,可以使用ThreadPoolExecutor.setMaximumPoolSize()方法来动态修改maximumPoolSize的值。
  5. 如果系统中的任务数量比较稳定,可以考虑将maximumPoolSize设置为一个固定的值,避免频繁地调整线程池的大小。

Untitled

keepAliveTime(空闲线程存活时间)

Java线程池的keepAliveTime参数表示空闲线程在被终止之前可以保持活动状态的时间。如果线程池中的线程数量大于核心线程数,那么这些线程在空闲状态下会等待新的任务到来,直到keepAliveTime时间到达为止。

使用时,可以通过调整keepAliveTime的值来控制线程池中空闲线程的数量和资源占用情况。如果将keepAliveTime设置得太短,会导致大量的空闲线程被创建和销毁,增加系统的开销和资源的浪费;如果将keepAliveTime设置得太长,又会导致系统资源的浪费和响应时间的延迟。

一般来说,可以根据具体的应用场景来选择合适的keepAliveTime值。如果需要快速响应任务请求,可以适当缩短keepAliveTime的值,以减少线程的创建和销毁次数;如果任务执行时间较长,可以适当延长keepAliveTime的值,以减少线程的频繁启动和停止对系统性能的影响。

unit(时间单位)

Java线程池的unit参数表示keepAliveTime的时间单位。常见的时间单位包括:

  • TimeUnit.SECONDS:秒
  • TimeUnit.MILLISECONDS:毫秒

workQueue(任务队列)

Java线程池的workQueue参数表示任务队列的类型和实现方式。在创建ThreadPoolExecutor对象时,需要指定一个实现了BlockingQueue接口的任务队列,该队列用于存放等待执行的任务。

常用的任务队列实现包括:

  1. ArrayBlockingQueue:基于数组实现的有界阻塞队列,适用于存储大量的任务。
  2. LinkedBlockingQueue:基于链表实现的有界阻塞队列,适用于存储少量的任务。
  3. SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待另一个线程的相应移除操作,通常用于实现生产者-消费者模式。构造函数,true:queue,false:stack。

在使用时,需要注意以下几点:

  1. 选择合适的任务队列类型和实现方式,根据具体的应用场景来选择。如果需要存储大量的任务,可以选择ArrayBlockingQueue;如果任务数量较少,可以选择LinkedBlockingQueue。
  2. 如果使用自定义的阻塞队列,需要确保其线程安全,避免出现死锁等问题。
  3. 如果使用SynchronousQueue作为任务队列,需要注意生产者和消费者之间的协作关系,避免出现死锁或者资源竞争等问题。
  4. 如果需要动态调整任务队列的大小,可以使用ThreadPoolExecutor.setRejectedExecutionHandler()方法来设置拒绝策略,或者使用ThreadPoolExecutor.setMaximumPoolSize()方法来动态调整线程池的大小。

threadFactory(线程工厂)

Java线程池的ThreadFactory参数是一个接口,用于创建新的线程。要创建一个自定义的ThreadFactory,需要实现以下方法:

public Thread newThread(Runnable r):创建一个新的线程,并将传入的Runnable对象作为线程的任务。
public ReturnCode createThread(Runnable r, String name, boolean isDaemon):创建一个新的线程,并根据传入的参数设置线程的名称、守护线程标志等属性。此方法在默认实现中不会使用。
以下是一个简单的自定义ThreadFactory实现示例:

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    public CustomThreadFactory(String namePrefix) {
        this.namePrefix = namePrefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + "-thread-" + threadNumber.getAndIncrement());
        if (t.isDaemon()) {
            t.setDaemon(false);
        }
        if (t.getPriority() != Thread.NORM_PRIORITY) {
            t.setPriority(Thread.NORM_PRIORITY);
        }
        return t;
    }
}

在这个示例中,我们创建了一个CustomThreadFactory类,它接受一个namePrefix参数,用于设置线程名称的前缀。newThread方法会创建一个新的线程,并设置线程的名称、守护线程标志和优先级。

rejectedExecutionHandler(拒绝策略)

Java线程池的rejectedExecutionHandler参数表示拒绝策略,用于处理当线程池中的线程数已经达到最大值时,新提交的任务无法被立即执行的情况。

常用的参数:

  1. 使用ThreadPoolExecutor.AbortPolicy()方法来抛出异常,中断任务的执行。
  2. 使用ThreadPoolExecutor.DiscardPolicy()方法来丢弃任务,不做任何处理。
  3. 使用ThreadPoolExecutor.CallerRunsPolicy()方法来将任务交给调用线程执行。

线程安全

多级缓存

多级缓存的基本思想是将数据存储在不同的缓存级别中,以最小化对主存的访问。第一级缓存通常是 CPU 缓存,它的访问速度最快,但容量很小。第二级缓存通常是 L2 缓存,它的访问速度稍慢,但容量更大。第三级缓存通常是主存,它的访问速度最慢,但容量最大。

一致性问题

在并发编程中,多级缓存可能会导致一致性问题。这是因为不同级别的缓存中的数据副本可能会不同步,导致数据不一致。

为了解决这个问题,可以采用以下几种方法:

  1. 缓存失效机制:可以设置缓存失效时间,并定期从主存中重新获取数据,以确保数据的一致性。
  2. 主动更新机制:当主存中的数据发生变化时,可以主动将缓存中的数据更新为主存中的数据,以确保数据的一致性。
  3. 锁机制:在并发访问缓存时,可以使用锁机制来保证数据的一致性。例如,可以使用读写锁来允许多个线程同时读取缓存,但只允许一个线程写入缓存。
  4. 事务机制:可以使用事务机制来确保缓存和主存中的数据一致性。事务可以包含多个操作,要么全部成功执行,要么全部回滚,以确保数据的一致性。

需要注意的是,在使用多级缓存时,需要根据实际情况选择合适的缓存级别和缓存策略,以确保数据的一致性和性能的最优化。

CPU时间片

CPU时间片是指在分时操作系统中,分配给每个正在运行的进程微观上的一段CPU时间。在抢占内核中,从进程开始运行直到被抢占的时间被称为时间片。现代操作系统允许同时运行多个进程,事实上,虽然一台计算机通常可能有多个CPU,但是同一个CPU永远不可能真正地同时运行多个任务。这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在Linux上为5ms-800ms),用户不会感觉到。

原子性问题

原子性问题是指在多线程环境下,一个或多个操作在CPU中执行的过程中出现被中断的情况,导致操作无法完整执行。

在多线程场景下,由于时间片切换的原因,原子性问题可能会出现。例如,线程A获得时间片开始执行,但在执行过程中,CPU时间片耗尽,线程A需要让出CPU,这时线程B获得了时间片开始执行。然而,对于线程A而言,它的操作可能并没有完全执行完成,也没有完全不执行,这就是原子性问题。

要解决原子性问题,可以采取以下措施:

  1. 使用同步机制:使用锁、信号量等同步机制来确保同一时间只有一个线程可以执行某项操作,避免多个线程同时访问和修改同一资源。
  2. 使用原子操作:对于简单的操作,可以使用原子操作来保证操作的完整性,原子操作是不可分割的,不会被其他线程中断的操作。
  3. 使用事务操作:对于需要执行一系列操作的复杂操作,可以使用事务操作来保证操作的原子性。事务操作可以将多个操作打包成一个事务,要么全部成功执行,要么全部回滚。

指令重排序

Java指令重排是指在Java程序执行过程中,指令的执行顺序与Java源代码中的顺序不一致的情况。Java指令重排通常是由于编译器优化、处理器乱序执行等原因引起的。

在Java程序中,指令重排可能会导致数据竞争等问题,因此需要进行相应的处理。以下是一些解决Java指令重排的方法:

  1. 使用synchronized关键字:synchronized关键字可以确保同一时间只有一个线程可以执行某个方法或某个代码块,避免多个线程同时访问和修改同一资源。
  2. 使用volatile关键字:volatile关键字可以确保变量的可见性,即当一个线程修改了某个变量的值后,其他线程可以立即看到修改后的值。使用volatile关键字可以避免由于指令重排导致的数据不一致问题。
  3. 使用锁:使用锁可以确保同一时间只有一个线程可以执行某个方法或某个代码块,避免多个线程同时访问和修改同一资源。
  4. 使用内存屏障:内存屏障是一种指令,用于确保指令的执行顺序。在Java程序中插入内存屏障可以阻止编译器优化和处理器乱序执行,确保指令的执行顺序与Java源代码中的顺序一致。

内存模型

线程安全是指在多线程环境下,程序的行为符合预期,不会出现数据不一致等问题。而内存模型是指虚拟机为了读取或者写入共享数据而从主内存中划分出一块区域,这个区域被称为工作内存。Java中的线程安全与内存模型密切相关,因为Java中的线程安全问题往往出现在访问共享数据时。

Java中的内存模型分为五个阶段:初始化、实例化、使用、赋值和销毁。在不同的阶段,内存中的值可能会被不同的线程访问,因此需要使用synchronized、volatile等关键字来保证线程安全。

happens-before

"happens-before"是Java内存模型中的一个概念,用于描述程序中变量之间的可见性和顺序性。

当两个线程中的操作A和B之间存在happens-before关系时,A操作的结果对B操作是可见的,即B操作可以在A操作之前或之后执行,而不会抛出异常。

happens-before关系分为以下四种类型:

  1. 程序顺序规则(Program Order):如果在一个线程中先于另一个线程运行一个语句,那么这个语句的执行顺序在另一个线程中是确定的。
  2. 锁定规则(Lock Ordering):如果一个线程获得了某个对象的锁,并且其他线程无法获得该对象的锁,那么其他线程必须等待该线程释放锁才能获得锁。
  3. volatile变量规则(Volatile Ordering):如果一个线程修改了一个volatile变量的值,那么其他线程可以立即看到这个修改后的值。
  4. 传递性规则(Transitivity):如果操作A happens-before 操作B,操作B happens-before 操作C,则操作A happens-before 操作C。

as-if-serial

As-if-serial语义是Java并发编程中的一个概念,它表示在单线程程序的执行过程中,无论编译器和处理器为了提高并行度如何进行重排序,其执行结果都不能被改变。

编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。

As-if-serial语义保护了单线程程序,使其不受重排序的影响,为单线程程序的编写者创造了一个幻觉:单线程程序是按程序的顺序来执行的。这个语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。不过需要注意的是,as-if-serial只保证单线程环境,在多线程环境下可能无效。

可重入锁

在Java中,所有的ReentrantLock实例都是可重入的,也就是说,一个线程可以多次获取同一个锁,只要每次获取都对应一次释放。

ReentrantLock lock = new ReentrantLock();  
  
// 一个线程  
lock.lock();  
lock.lock(); // 可以再次获取,因为是可重入的  
try {  
    // 执行同步代码块  
} finally {  
    lock.unlock(); // 释放一次  
    lock.unlock(); // 释放第二次  
}

以下是可重入锁的一些重要特性:

  1. 可重入性:一个线程可以多次获取同一个锁,只要每次获取都对应一次释放。
  2. 公平性:可重入锁可以在构造函数配置为公平锁或非公平锁。公平锁意味着等待时间最长的线程将首先获得锁,而非公平锁则不保证这一点。
  3. 锁状态:可重入锁可以查询其状态,例如查询是否被某个线程持有。
  4. 锁定和解锁:可重入锁提供了lock()和unlock()方法来手动控制锁的获取和释放。
  5. 等待和通知:可重入锁可以与Condition对象一起使用,以实现等待/通知模式。

阻塞锁

如果你想要实现一个阻塞锁,你可以使用lock()方法。如果锁被其他线程持有,那么当前线程将会被阻塞,直到锁被释放。

数据库锁

数据库锁机制是用于在并发访问数据库时,协调多个事务对同一资源的访问顺序,以保证数据一致性的技术。

根据锁定资源的粒度,可以将数据库锁分为以下类型:

  1. 数据库级锁:锁定整个数据库。
  2. 表级锁:锁定一张表。
  3. 区域级锁:锁定数据库的特定区域。
  4. 页面级锁:锁定数据库的特定页面。
  5. 见面级锁:锁定数据库的特定页面。
  6. 键值级锁:锁定数据库表中带有索引的一行数据。
  7. 行级锁:锁定数据库表中的但行数据(即一条记录)。

根据封锁的程度,可以将数据库锁分为以下类型:

  1. 共享锁:用于读数据操作,它是非独占的,允许其他事务同时读取其锁定的资源,但不允许其他事务更新数据。
  2. 独占锁:独占锁用于写数据操作,独占锁在持有期间,其他事务不能读取也不能更新被锁定的资源。

分布式锁

  1. 基于数据库的分布式锁

在这种方式下,我们可以使用数据库中的表来作为锁对象。当一个线程需要获取锁时,它向数据库中插入一条记录,如果记录插入成功则表示该线程获取到了锁;否则表示其他线程已经获取了锁。需要注意的是,这种方式需要保证数据库的一致性和可靠性。

public class DatabaseLock {
    private final String lockKey = "lock_key";
    private final DataSource dataSource;

    public DatabaseLock(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public boolean tryLock() {
        try (Connection connection = dataSource.getConnection();
            PreparedStatement statement = connection.prepareStatement("INSERT INTO lock_table (lock_key, lock_value) VALUES (?, ?)")) {
            statement.setString(1, lockKey);
            statement.setString(2, "locked");
            int result = statement.executeUpdate();
            return result == 1;
        } catch (SQLException e) {
            throw new RuntimeException("Failed to acquire database lock", e);
        }
    }

    public void unlock() {
        try (Connection connection = dataSource.getConnection();
             PreparedStatement statement = connection.prepareStatement("DELETE FROM lock_table WHERE lock_key = ?")) {
            statement.setString(1, lockKey);
            statement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("Failed to release database lock", e);
        }
    }
}
  1. 基于 Redis 的分布式锁

在这种方式下,我们可以使用 Redis 中的 setnx 命令来实现分布式锁。setnx 命令可以在 Redis 中设置一个 key-value 键值对,并指定该 key 的值为 value。如果该 key 不存在,则设置成功并返回 1;否则设置失败并返回 0。我们可以利用这个特性来实现分布式锁:当一个线程需要获取锁时,它尝试使用 setnx 命令设置一个特定的 key-value 键值对,如果设置成功则表示该线程获取到了锁;否则表示其他线程已经获取了锁。

import redis.clients.jedis.Jedis;

public class RedisLock {
    private final String lockKey = "lock_key";
    private final Jedis jedis;

    public RedisLock(Jedis jedis) {
        this.jedis = jedis;
    }

    public boolean tryLock() {
        Long result = jedis.setnx(lockKey, "locked");
        return result == 1;
    }

    public void unlock() {
        jedis.del(lockKey);
    }
}
  1. 基于 ZooKeeper 的分布式锁

在这种方式下,我们可以使用 ZooKeeper 提供的临时顺序节点来实现分布式锁。具体来说,我们可以在某个节点上创建一个有序的子节点列表,并在需要获取锁时判断自己创建的节点是否是当前最小的节点(即序号最小的节点)。如果是最小节点,则表示该线程获取到了锁;否则表示其他线程已经获取了锁。需要注意的是,这种方式需要保证 ZooKeeper 的高可用性和一致性。

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class ZooKeeperLock {
    private static final String ZK_LOCK_ROOT_PATH = "/locks";
    private static final String ZK_LOCK_NODE_NAME = "lock_node";
    private ZooKeeper zk;
    private String lockPath;
    private CountDownLatch lockAcquiredSignal;

    public ZooKeeperLock(ZooKeeper zk) throws IOException {
        this.zk = zk;
        this.lockPath = ZK_LOCK_ROOT_PATH + "/" + System.currentTimeMillis();
        this.lockAcquiredSignal = new CountDownLatch(1);
    }

    public boolean tryLock() throws KeeperException, InterruptedException {
        zk.create(lockPath, null, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        String lockNodePath = lockPath + "/" + ZK_LOCK_NODE_NAME;
        Stat stat = zk.exists(lockNodePath, false);
        if (stat == null) {
            lockAcquiredSignal.await();
            zk.create(lockNodePath, null, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            return true;
        } else {
            return false;
        }
    }

    public void unlock() throws InterruptedException, KeeperException {
        zk.delete(lockPath + "/" + ZK_LOCK_NODE_NAME, -1);
        zk.close();
        lockAcquiredSignal.countDown();
    }
}

无锁

CAS

CAS(Compare and Swap)是一种无锁的同步机制,它通过比较并交换内存中的值来实现线程安全。在Java中,可以使用java.util.concurrent.atomic包下的原子类(如AtomicInteger、AtomicLong等)来实现CAS操作。

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(0); // 创建一个初始值为0的AtomicInteger对象
        int oldValue;
        int newValue;
        do {
            oldValue = atomicInteger.get(); // 获取当前值
            newValue = oldValue + 10; // 计算新值
        } while (!atomicInteger.compareAndSet(oldValue, newValue)); // 比较并设置新值,如果失败则继续循环
        System.out.println("最终值为:" + atomicInteger.get()); // 输出最终值
    }
}

在上面的代码中,首先创建了一个初始值为0的AtomicInteger对象。然后使用do-while循环进行CAS操作,即先获取当前值,再计算新值,最后比较并设置新值。如果比较结果为false,表示当前值没有被其他线程修改过,可以更新为新值,否则继续循环。最终输出AtomicInteger对象的值。

需要注意的是,AtomicInteger的compareAndSet方法要求参数类型为int,因此在使用时需要将long类型的值转换为int类型。此外,AtomicInteger还提供了其他一些常用的方法,如getAndIncrement、getAndDecrement等,可以根据实际需求选择使用。

ABA 问题

ABA问题产生的原因是在多线程环境下,使用CAS操作时,可能会出现以下两种情况:

  1. 数据竞争:多个线程同时读取同一个变量的值,然后进行修改,最后再写回。由于线程执行顺序的不确定性,可能会导致某个线程在读取变量值后,其他线程已经修改了该变量的值,从而导致写入失败。
  2. 空指针异常:当一个对象被多个线程共享时,如果其中一个线程对该对象进行了修改,而其他线程仍然使用了旧的对象引用,就会导致空指针异常。

为了解决ABA问题,可以使用以下方法:

  1. 使用AtomicStampedReference来避免ABA问题的复现。AtomicStampedReference是一个带有时间戳的引用类型,可以保证在多线程环境下对引用对象的修改和读取都是原子性的。
  2. 使用volatile关键字来保证可见性。volatile关键字可以保证对共享变量的修改对所有线程立即可见,从而避免了数据竞争的问题。
  3. 使用synchronized关键字来保证互斥性。synchronized关键字可以保证在同一时刻只有一个线程能够访问共享资源,从而避免了数据竞争和空指针异常的问题。
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
    private static AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(0, 0);

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                long stamp = stampedRef.getAndIncrement(); // 获取当前时间戳并加1
                if (stamp == Integer.MAX_VALUE) { // 如果时间戳等于Integer的最大值,说明发生了溢出
                    System.out.println("ABA problem occurred!");
                } else {
                    System.out.println("No ABA problem occurred.");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                stampedRef.compareAndSet(i, i + 1, stampedRef.get(i), stampedRef.get(i + 1)); // 比较并设置新值
            }
        });

        t1.start();
        t2.start();
    }
}

在上面的代码中,使用AtomicStampedReference来保证对共享变量的修改和读取都是原子性的。在每个线程中,通过调用getAndIncrement()方法获取当前时间戳并加1,然后判断时间戳是否等于Integer的最大值,如果是则说明发生了溢出,否则说明没有发生ABA问题。在另一个线程中,通过调用compareAndSet()方法比较并设置新值,从而保证了对共享变量的正确修改。

锁优化

偏向锁

public class LockDemo {
    private static volatile int count = 0; // 定义一个volatile变量count,用于实现偏向锁

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                increment(); // 调用increment方法进行加1操作
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                increment(); // 调用increment方法进行加1操作
            }
        });

        t1.start();
        t2.start();
    }

    public static void increment() {
        for (int i = 0; i < 10; i++) { // 模拟加锁操作需要多次尝试的情况
            int temp = count; // 将count的值保存到临时变量temp中
            count = temp + 1; // 对count进行加1操作
            if (count == 0) { // 如果count为0,则表示未被锁定,需要进行加锁操作
                synchronized (LockDemo.class) { // 使用synchronized关键字进行加锁操作
                    temp = count; // 再次将count的值保存到临时变量temp中
                    count = temp + 1; // 对count进行加1操作
                }
            } else { // 如果count不为0,则表示已被锁定,直接返回
                return;
            }
        }
    }
}

在上面的代码中,使用volatile关键字修饰count变量,保证其可见性。在increment()方法中,首先将count的值保存到临时变量temp中,然后对count进行加1操作。如果count为0,则表示未被锁定,需要进行加锁操作;如果count不为0,则表示已被锁定,直接返回。在进行加锁操作时,使用synchronized关键字对整个类进行加锁,以保证线程安全。

锁消除

锁消除是指JVM在编译时对代码进行分析和优化,将一些不必要的锁操作消除掉,从而提高程序的执行效率。具体来说,锁消除可以通过以下两种方式实现:

  1. 锁粗化:将多个连续的加锁操作合并成一个锁操作,从而减少锁的开销。例如,可以将多个synchronized块合并成一个synchronized块,或者将多个synchronized方法调用合并成一个synchronized方法调用。
  2. 锁细化:将一些不必要的锁操作消除掉,从而提高程序的执行效率。例如,可以使用volatile关键字代替synchronized关键字来实现线程安全,或者使用Atomic类来实现原子操作。

线程相关方法

wait & notify

public class WaitNotifyDemo {
    private static final Object lock = new Object();
    private static boolean flag = true;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    try {
                        while (!flag) {
                            lock.wait();
                        }
                        Thread.sleep(500);
                        System.out.println("Thread 1: Hello from Thread 1");
                        flag = false;
                        lock.notify();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    try {
                        while (flag) {
                            lock.wait();
                        }
                        Thread.sleep(500);
                        System.out.println("Thread 2: Hello from Thread 2");
                        flag = true;
                        lock.notify();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        t1.start();
        t2.start();
    }
}

ThreadLocal

原理

ThreadLocal是Java提供的一种线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以再任意时刻、任意方法中获取缓存的数据。ThreadLocal的底层实现原理是通过一个Map来存储每个线程对应的变量值,Map的key为变量名,value为变量值。ThreadLocalMap是一个继承自AbstractMap的子类,它重写了put、get、remove等方法,使得每个线程只能访问自己的变量值,从而实现了线程间数据隔离 。

底层数据结构

ThreadLocal的底层数据结构是一个继承自AbstractMap的子类ThreadLocalMap,它重写了put、get、remove等方法,使得每个线程只能访问自己的变量值,从而实现了线程间数据隔离 。ThreadLocalMap是一个数组,键值对Entry(ThreadLocal k, Object v)都存储在table数组中 。

并发包

同步容器 并发容器

同步容器和并发容器是Java中的两种容器类型。同步容器是指通过synchronized关键字修饰容器保证同一时刻内只有一个线程在使用容器,从而使得容器线程安全。而并发容器指的是允许多线程同时使用容器,并且保证线程安全。为了达到尽可能提高并发,Java并发工具包中采用了多种优化方式来提高并发容器的执行效率,核心的就是:锁、CAS(无锁)、COW(读写分离)、分段锁等 。

Callable

Callable是Java中的一个接口,它表示可以返回结果的任务。与Runnable不同,Callable任务可以抛出异常,并且可以有返回值。Callable接口与Future接口关联,通过Future对象可以获取Callable任务的执行结果或者取消任务等操作。

实现Callable接口需要实现call()方法,该方法返回一个结果,可以是任意类型。如果计算过程中抛出异常,则会在call()方法中抛出相同的异常。

使用Callable和Future可以实现异步编程,将耗时的操作放在后台线程中执行,而主线程可以继续处理其他任务,等到后台任务执行完成后再获取结果。这种方式可以提高程序的效率和响应速度。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableDemo implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        // 这里是需要执行的任务,返回一个整数类型的结果
        int result = 1;
        Thread.sleep(3000);
        return result;
    }

    public static void main(String[] args) throws Exception {
        // 创建一个Callable对象
        CallableDemo callableDemo = new CallableDemo();
        // 将Callable对象包装成FutureTask对象
        FutureTask<Integer> futureTask = new FutureTask<>(callableDemo);
        // 创建线程并执行任务
        Thread thread = new Thread(futureTask);
        thread.start();

        // 等待并获取任务执行结果,阻塞了
        Integer result = futureTask.get();
        System.out.println("任务执行结果:" + result);
    }
}

CountDownLatch

CountDownLatch是Java并发编程中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。

CountDownLatch的原理是通过一个计数器来实现的。在创建CountDownLatch对象时,需要指定计数器的初始值。每当一个线程完成任务后,就会调用countDown()方法将计数器减1。当计数器减为0时,表示所有线程都已经完成操作,此时可以通过await()方法让主线程等待,直到计数器变为0后再继续执行。

CountDownLatch的用法如下:

  1. 创建一个CountDownLatch对象,并指定计数器的初始值。
  2. 创建线程并启动它们,每个线程在完成任务后调用countDown()方法将计数器减1。
  3. 在主线程中调用await()方法,等待计数器变为0后再继续执行。
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个CountDownLatch对象,初始计数器为3
        CountDownLatch countDownLatch = new CountDownLatch(3);

        // 创建并启动3个线程
        for (int i = 1; i <= 3; i++) {
            final int index = i;
            new Thread(() -> {
                System.out.println("线程" + index + "正在执行任务...");
                // 模拟任务执行时间
                try {
                    Thread.sleep(1000L * index);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程" + index + "任务执行完毕!");
                // 计数器减1
                countDownLatch.countDown();
            }).start();
        }

        // 主线程等待所有子线程执行完毕
        countDownLatch.await();
        System.out.println("所有子线程任务都执行完毕了!");
    }
}

CyclicBarrier

CyclicBarrier是Java并发编程中的一个同步工具类,它允许一组线程相互等待,直到所有线程都到达一个同步点后再一起继续执行。

CyclicBarrier的原理是通过一个计数器来实现的。在创建CyclicBarrier对象时,需要指定计数器的初始值。每当一个线程到达同步点时,就会调用await()方法将计数器减1。当计数器减为0时,表示所有线程都已经完成操作,此时所有线程可以继续执行。

CyclicBarrier的用法如下:

  1. 创建一个CyclicBarrier对象,并指定计数器的初始值。
  2. 创建线程并启动它们,每个线程在到达同步点后调用await()方法将计数器减1。
  3. 在主线程中调用await()方法,等待计数器变为0后再继续执行。

下面是一个简单的示例代码:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeoutException;

public class CyclicBarrierDemo {
    public static void main(String[] args) throws InterruptedException, BrokenBarrierException, TimeoutException {
        long time1 = System.currentTimeMillis();

        // 创建一个CyclicBarrier对象,并指定计数器的初始值为3
        CyclicBarrier cyclicBarrier = new CyclicBarrier(4);

        // 创建3个线程
        for (int i = 0; i < 3; i++) {
            final int index = i;
            new Thread(() -> {
                System.out.println("线程" + index + "开始执行任务...");
                // 模拟任务执行时间
                try {
                    Thread.sleep((5 - index) * 500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程" + index + "任务执行完毕!");
                try {
                    // 到达同步点,计数器减1
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    throw new RuntimeException(e);
                }
            }).start();
        }

        // 主线程等待计数器变为0
        cyclicBarrier.await();

        long time2 = System.currentTimeMillis();
        System.out.println("所有线程任务都执行完毕了,用时:" + (time2 - time1) + "ms");
    }
}

在上面的代码中,我们创建了一个初始计数器为3的CyclicBarrier对象,并创建了3个线程来执行任务。每个线程在完成任务后会调用await()方法将计数器减1。主线程通过调用await()方法等待计数器变为0后再继续执行。最后输出所有线程任务都执行完毕了的提示信息。