Java
Java集合
Collection
map
HashMap
-
jdk1.7以前,HashMap是基于哈希表(数组+链表)来实现,通过一个哈希函数,将元素散列到哈希表里。
Java7 HashMap采用的是冲突链表方式
jdk1.8后,HashMap采用的是数组+链表+红黑树的方法,链表长度超过一定阈值(默认为8),HashMap会将链表转换为红黑树,查找效率也从O(n)变为O(log N)的数量级。
多线程
1.生命周期
就绪状态:表示当前线程具备抢夺cpu时间片的权利。当抢到cpu的时间片后,就开始执行run方法,run方法执行即为运行态
运行状态:run方法执行表示线程进入运行态,当有多个线程开启Thread.start()想访问cpu资源,cpu会通过时间片轮转的方法, 一个线程所占有的时间片用完,cpu就会给另一个线程分配时间片,执行另一个线程的run方法,多线程的run方法才会交替执行
阻塞状态:当一个线程遇到阻塞事件,如键盘输入,或者sleep(),就会释放cpu占用的时间片,此时需要再次回到就绪状态重写获取cpu调度
thread.join(): 当主线程需要用到子线程结束后的结果,需要thread.join(),主线程下面的代码就会等到子线程结束后再执行
3. 线程控制
方法名 | 说明 |
---|---|
void yield() | 使当前线程让步,重新回到争夺CPU执行权的队列中 |
static void sleep(long ms) | 进入阻塞态,使当前正在执行的线程停留指定的毫秒数 |
void join() | t1.join()后,t1线程会被cpu调度,而主线程阻塞,不能被cpu调度,当t1线程执行完后,主线程才重新被调度 |
void interrupt() | 中止线程睡眠,如果线程调用sleep(), interrupt()可以将它唤醒 |
2. 基本用法
1.继承Thread类
class MyThread extends Thread {
public void run() {
// 线程要执行的任务
}
}
MyThread thread = new MyThread();
thread.start();
2. 实现Runable接口
class MyRunnable implements Runnable {
public void run() {
// 线程要执行的任务
}
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
或者
Thread thread1 = new Thread(new Runnable() {
//直接new线程重写Runnable接口
@Override
public void run() {
synchronized (buffer){
for (int i = 0; i < 100; i++) {
buffer.append("A");
}
}
}
});
thread.start();
4.线程安全
4.1竞争条件
竞态条件(Race Condition):竞态条件是指多个线程同时访问共享资源时,由于执行时序不确定导致的问题。例如,两个线程同时对一个变量进行读写操作,由于执行时序不确定,可能会导致不一致的结果。竞态条件可以通过使用同步机制来避免。
4.2死锁
死锁:发生在两个或多个线程互相等待对方持有的资源时,导致线程都无法继续执行的状态
简单的解决办法:
- 按顺序上锁:确保线程获取锁的顺序是固定的
- 使用
synchronized
关键字的内置锁: 避免在一个线程中获取多个锁,如果必须要获取多个锁,尽量按照固定的顺序获取,避免产生死锁
举例:
两个线程,它们都要2个资源,线程1拥有资源1,等待资源2,线程2拥有资源2,等待资源1,
他们互相持有对方资源不放,这样出现了一种循环等待的情况。(代码上,是一个线程里采用多个锁,且顺序不当导致的)
以下代码,最终就是循环等待,出现了死循环
public class ThreadLockTest {
private static final Object res1 = new Object();
private static final Object res2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (res1){
System.out.println("Thread1 我拥有了资源1");
try {
Thread.sleep(100); //给个延迟,让线程二有res2
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (res2){
System.out.println("Thread2 拥有了res2");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (res2){ //锁住资源二
System.out.println("Thread2 我有资源2了");
try {
Thread.sleep(100); //给个延迟,让线程1有res1
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (res1){
System.out.println("Thread2 拥有了res1");
}
}
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end............"); //没结束,发送了死循环
}
}
4.3 volatile
volatile
是一个关键字,用于修饰变量,任何一个线程对该变量修改后可以立即写回主存
当一个变量被 volatile
修饰时,意味着这个变量的值可能会被多个线程同时修改,因此在读取和写入这个变量时需要一些额外的保证,以确保线程之间能够正确地读取到最新的值,并且保证写入操作对其他线程可见。
具体来说,volatile
关键字主要提供了以下两个特性:
- 可见性(Visibility):当一个变量被
volatile
修饰时,任何一个线程对该变量的修改都会立即写回主内存,并且其他线程在读取该变量时会从主内存中重新获取最新的值,而不是从线程的工作内存中获取。 - 禁止指令重排序(Prevent Instruction Reordering):
volatile
关键字还可以禁止编译器和处理器对被修饰变量相关的读写操作进行重排序,保证了代码执行的顺序与程序员编写的顺序一致。
flag
是 volatile
类型的变量,因此在一个线程修改 flag
的值时,另一个线程可以立即看到这个修改,从而实现了可见性
public class VolatileExample {
private volatile boolean flag = true;
public void reverseFlag(){
flag = !flag;
}
public boolean getFlag() {
return flag;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
//负责修改flag
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
example.reverseFlag();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
// 线程2:负责读取flag的值
Thread t2 = new Thread(() -> {
while (true) {
System.out.println("Flag value: " + example.getFlag());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
String,StringBuffer和StringBuilder
String:
String
是不可变的。一旦创建了一个String
对象,它的值就不能被改变。- 由于不可变性,对
String
的任何修改都会创建一个新的String
对象,这可能导致内存开销和性能问题,特别是在频繁操作大量字符串时。 String
是线程安全的,因为它的不可变性保证了多线程环境下的安全性。
StringBuffer:
StringBuffer
是可变的,它提供了对字符串内容的修改操作,如添加、插入、删除等。StringBuffer
是线程安全的,所有的方法都是同步的,这意味着可以在多线程环境下安全地使用它。然而,这也导致了一定的性能开销。
底层:采用synchronized关键字
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
举例:
public class StringTest {
public static void main(String[] args) throws InterruptedException {
//创建buffer对象
StringBuffer buffer = new StringBuffer();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (buffer){
for (int i = 0; i < 100; i++) {
buffer.append("A");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (buffer){
for (int i = 0; i < 100; i++) {
buffer.append("B");
}
}
}
});
thread1.start();
thread2.start();
// 等待两个线程结束
thread1.join();
thread2.join();
Thread.sleep(100);
System.out.println("Final StringBuffer content: " + buffer.toString());
}
}
StringBuilder(多线程不安全,单线程安全的):
StringBuilder
也是可变的,类似于StringBuffer
,但是它不是线程安全的。- 由于不需要同步控制,
StringBuilder
的性能通常比StringBuffer
更高,特别是在单线程环境下。 - 在单线程环境下,如果不需要线程安全保证,通常建议使用
StringBuilder
而不是StringBuffer
。
设计模式
1. 单例模式
单例模式是一种设计模式,用于整个应用只有一个实例,并提供一个全局访问点来访问该实例
使用场景:单例模式常用于管理全局状态或资源,如配置信息、数据库连接池、线程池等
饿汉式
饿汉式:在饿汉式中,单例对象在类加载时就被创建,因此在应用程序启动时就会创建该实例
public class Singleton {
// 在类加载时即创建单例对象
private static final Singleton instance = new Singleton();
// 私有构造方法,防止外部实例化
private Singleton() {
}
// 提供全局访问点
public static Singleton getInstance() {
return instance;
}
}
优缺点:饿汉式的优点是实现简单,线程安全,不存在多线程竞争的问题。但缺点是可能会导致资源浪费,因为在应用程序启动时就会创建实例,如果实例长时间未被使用,会占用内存空间
懒汉式
懒汉式:在懒汉式中,单例对象在首次被使用时才被创建,这样可以避免资源的浪费
public class Singleton {
// 延迟初始化单例对象,使用volatile关键字保证多线程安全性
private static volatile Singleton instance;
// 私有构造方法,防止外部实例化
private Singleton() {}
// 提供全局访问点,双重检查锁定确保线程安全
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
优缺点:懒汉式的优点是避免了资源浪费,只有在需要时才会创建实例。但需要注意的是,懒汉式在多线程环境下需要考虑线程安全性,可以通过双重检查锁定(Double-Checked Locking)等方式来保证线程安全。
2. 工厂模式
工厂模式是一种创建型设计模式,将对象的实例化过程封装在工厂类中,客户端只需要通过工厂类来请求所需的对象,而无需了解具体对象是如何创建的。
优点:
- 统一的接口:工厂模式可以定义一个统一的接口或抽象类来表示不同类型的指令,客户端只需要知道如何使用这个接口或抽象类,而无需关心具体的实现细节。
- 封装对象创建逻辑:工厂模式将对象的创建过程封装在工厂类中,客户端无需关心对象的创建过程,只需要调用工厂类的方法来获取所需的对象。
- 易于扩展:如果需要新增或修改指令的处理方式,只需在工厂类中新增或修改相应的创建逻辑,而无需修改客户端的代码,符合开闭原则,易于扩展。
- 提高代码的灵活性和可维护性:工厂模式可以将对象的创建和使用分离,降低了代码的耦合度,使得代码更加灵活和易于维护。