程序中的错误统称为异常。
可以举例说明,一个公共变量a,三个线程,一个线程修改了a的值,其他两个线程可能看不到a变化后的值,这就是内存可见性问题。
原因是:为了提高处理速度,每个线程都会在 CPU 中维护一份共享变量的本地缓存,而线程对共享变量的所有操作都会在自己的本地缓存中进行。如果线程 A 更改了一个共享变量,线程 B 有可能看不到线程 A 的修改
解决方法:
使用volatile关键字:
将变量声明为volatile,这会告诉Java虚拟机确保所有线程都能看到最新的值。
当一个线程修改了volatile变量的值,这个变化会立即被写入主内存,并且其他线程在读取该变量时会从主内存中获取最新值。
使用synchronized关键字:
使用synchronized块来对读写操作进行同步,确保同一时间只有一个线程能够访问共享变量。
当一个线程获取了锁并修改了共享变量后,其他线程必须等待该线程释放锁才能访问该变量,这样可以确保可见性。
使用java.util.concurrent包中的工具类:
Java提供了一些并发工具类,如AtomicInteger、CountDownLatch、CyclicBarrier等,它们可以用来处理多线程可见性问题,而无需手动编写同步代码。
这些工具类提供了原子操作和同步机制,可以确保对共享变量的修改对其他线程可见。
volatile 是 Java 中的关键字,用于修饰变量。它的主要作用是确保多线程环境下的可见性和有序性,这意味着当一个线程修改了 volatile 变量的值时,其他线程可以立即看到这个修改。
volatile关键字的作用:
适用场景: volatile 适用于一些简单的标志位或状态标识的操作,例如线程之间的信号通知。它不适合复杂的操作,如累加操作。
(1)接口的默认方法和静态方法:之前接口只能够做方法的声明,没有实现,Java8以后允许接口有一个默认的实现,必须使用default修饰符标记;
default void test(){}
static void test2(){}
(2)Lambda表达式:Lambda最直观的是将代码变得整洁。
(3)函数式接口:
(4)方法引用:是用来直接访问类或者实例中的方法或者构造方法,这样代码的可读性会更高一些。
就是使用::
来调用类中的方法:
在这里插入代码片
(5)Stream流:它允许你以声明式的方式处理数据集合。
select.stream().parallel()
(6)Optional:为了解决空指针异常。并且让代码更加简洁,使用它我们不需要显式的进行空指针检测。
Optional + lambda实现比较字符串,并找到最长的字符串
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class OptionalLambdaExample {
public static void main(String[] args) {
List<String> stringList = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");
Optional<String> longestString = stringList.stream()
.reduce((s1, s2) -> s1.length() > s2.length() ? s1 : s2);
longestString.ifPresent(s -> System.out.println("最长的字符串是: " + s));
}
}
(7)Date/Time
(8)重复注解
(9)扩展注解的支持
(10)Base64
(11)JavaFx
synchronized 是Java的关键字,直接内置在Java语言中。您可以使用synchronized关键字来实现同步块或同步方法。
Lock 是一个接口,它在java.util.concurrent.locks包中定义。Java提供了多种Lock的实现,包括ReentrantLock、ReadWriteLock等。您可以使用这些Lock实现来管理同步。
Lock 提供了更多的灵活性。您可以使用Lock接口的不同实现来满足特定的同步需求。例如,ReentrantLock支持可重入锁,而ReadWriteLock支持读写锁。
synchronized更简单,但在某些情况下可能不够灵活,例如无法轻松实现尝试锁定、定时锁定等功能。
synchronized关键字用于锁定整个方法或代码块,这可能会导致性能问题,特别是在高并发情况下。
Lock允许您更细粒度地控制锁的范围,可以只锁定需要同步的关键部分,从而提高并发性能。
使用synchronized关键字时,如果发生异常,锁将自动释放。
使用Lock时,您需要在try-finally块中手动释放锁,以确保在发生异常时锁定资源得到释放。
Lock接口提供了Condition对象,可以用于实现条件等待和通知机制。这使得线程能够更灵活地等待某些条件的发生,而不需要一直忙等。
总之,synchronized适合简单的同步需求,而Lock适用于更复杂、灵活的同步需求。选择哪种同步方式取决于您的具体需求和性能考虑。在Java并发编程中,通常建议优先使用Lock接口,因为它提供了更多的控制和灵活性。但要注意,Lock使用起来相对复杂,需要小心处理异常和锁定资源的释放。
sleep() 用于线程的暂时休眠,不释放锁,通常用于时间延迟。
wait() 用于线程之间的等待和唤醒机制,会释放锁,必须在synchronized块中使用。
sleep()是Thread类的方法,而wait()是Object类的方法。
(1)新生代: 新生代是堆内存的一部分,主要用于存储新创建的对象。新生代:
- Eden
空间:新创建的对象首先分配到Eden
空间。
- Survivor
空间(S0和S1):当Eden
空间填满时,存活下来的对象会被移动到Survivor
空间。Survivor
空间有两个,通常标记为S0
和S1
,它们交替用作对象的复制和垃圾回收。经过多次复制和存活的对象最终会被晋升到老年代。
(2)老年代: 老年代用于存储长时间存活的对象。当对象在新生代经过多次复制后仍然存活,它们会被晋升到老年代。老年代中的对象在垃圾回收时会经历更长的生命周期。
(3)永久代: 永久代用于存储类的元数据信息,如类定义、方法信息、常量池等。它的大小在JVM启动时被固定分配,不会自动扩展。在旧版本的JVM中,永久代可能会导致内存溢出问题,因此在JVM 8中被元数据区所取代。
(4)元空间: 元数据区是用于存储类的元数据信息的内存区域。与永久代不同,元数据区的大小是动态分配的,它可以根据应用程序的需求而自动扩展或收缩。在JVM 8及更高版本中,元数据区取代了永久代,这解决了永久代导致的一些内存溢出问题。
类对象的晋升方式:
(1)对象分配到新生代(Eden空间): 当一个对象被创建时,它通常被分配到新生代的Eden空间。这是新对象的初始分配区域。
(2)Minor Garbage Collection(新生代垃圾回收): 新生代会定期进行垃圾回收,通常采用复制(Copying)算法。在这个过程中,存活的对象会被复制到Survivor空间(S0或S1),而不存活的对象会被回收。
(3)对象在Survivor空间中晋升: 如果对象在新生代经历了一定次数的垃圾回收后仍然存活,它会被晋升到老年代。这个阈值通常由JVM参数控制。
(4)Major Garbage Collection(老年代垃圾回收): 老年代的垃圾回收发生相对较少,因为其中存储的对象通常具有较长的生命周期。老年代的垃圾回收通常使用标记-清除(Mark and Sweep)算法。
(5)Full Garbage Collection(全局垃圾回收): 如果老年代垃圾回收无法回收足够的内存,JVM可能会触发全局垃圾回收,它会回收整个堆内存,包括新生代和老年代。
晋升到老年代的对象经历了多次新生代垃圾回收,这意味着它具有较长的生命周期,通常是应用程序中的长期存活对象。这种分代垃圾回收策略有助于提高垃圾回收的效率,因为大多数对象都在新生代很快被回收,而只有一小部分对象会晋升到老年代,减少了老年代的垃圾回收频率,从而减少了应用程序的停顿时间。