赞
踩
我们将从以下四个问题入手,对Java的多线程问题抽丝剥茧。
线程安全指的是多个线程并发访问共享资源时,不会出现数据不一致或其他意外情况的情况。在多线程编程中,线程安全非常重要,因为多个线程可能会同时访问和修改同一数据,如果不进行适当的同步处理,就可能导致数据不一致、竞态条件和死锁等问题。
为了实现线程安全,需要使用一些技术和方法来保证数据的一致性和同步性,例如锁机制、原子操作、线程局部变量等。常用的线程安全类包括Vector、CopyOnWriteArrayList、Hashtable、ConcurrentHashMap、原子类等。
在Java中,线程安全可以通过以下几种方式实现:
synchronized的实现原理:被synchronized修饰的代码块称为同步块,当线程进入同步块时,会尝试获取对象的锁,如果对象没有被加锁或者已经获取了该对象的锁,则锁计数器+1;如果该对象已经被其他线程加锁,则该线程会进入阻塞状态,等待其他线程释放锁。当其他线程释放锁后,等待的线程会被唤醒,并重新尝试获取锁并执行同步块中的代码。同一时刻,只有一个线程可以获取对象的锁并执行同步块中的代码。
对象头:在Java中,每个对象都有一个对象头(Object
Header),它用于存储对象的元数据,包括对象的哈希码(hashCode)、锁状态、GC标记状态等信息。对象头的大小是固定的,通常占用8个字节(64位系统)或4个字节(32位系统)。对象头中最重要的信息是锁状态,用于实现Java中的synchronized关键字的同步机制。锁状态的值可以是无锁状态、偏向锁状态、轻量级锁状态或重量级锁状态,锁状态取决于线程之间的竞争情况和锁的使用方式。
以下方法为synchronized关键字的使用:
public class Counter {
private int count;
// synchronized 修饰方法
public synchronized void increment() {
count++;
}
// synchronized 修饰代码块
public void add(int n) {
synchronized (this) {
count += n;
}
}
}
使用synchronized关键字的注意点:
优点:简单易用、支持可重用锁。
缺点:性能问题、只能保护代码块或方法。
可见性:如果两个线程同时对一个volatile变量进行修改,由于volatile变量能够保证可见性,那么它们的修改结果都会被立即刷新到主内存中,从而使得另外一个线程可以读取到最新的值。
读取操作顺序与写入操作顺序一致:如果一个线程读取了volatile变量,而在它进行写入之前,另一个线程也读取了同一个volatile变量,那么在第一个线程写入变量之后,另一个线程读取到的变量值是第一个线程写入的最新值,而不是读取时的值。
指令重排:是指处理器或编译器为了优化程序执行效率,在不改变原有程序执行结果的前提下,改变指令的执行顺序,以达到减少指令执行的等待时间、利用处理器的多级流水线、减少分支预测错误等目的。在单线程环境下,指令重排不会带来任何问题,因为最终执行结果不会发生变化。但在多线程环境下,指令重排可能会导致一些意料之外的结果,例如数据不一致、死锁、无限循环等问题。
以下方法为volatile关键字的使用:
public class VolatileExample {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
优点:变量对于所有线程的可见性、顺序读写
缺点:不能保证原子性、频繁读写volatile变量的开销大
可重入锁是指可以对同一个锁进行重复的加锁和解锁操作,每次加锁操作都必须对应一个解锁操作,否则锁将一直被占用。synchronized关键字也是可重入锁。
以下方法为ReentrantLock可重入锁的使用:
import java.util.concurrent.locks.ReentrantLock; public class Demo { private ReentrantLock lock = new ReentrantLock(); public void method1() { lock.lock(); try { System.out.println("method1"); method2(); } finally { lock.unlock(); } } public void method2() { lock.lock(); try { System.out.println("method2"); } finally { lock.unlock(); } } }
可重入锁的特点:
重复加锁:内部有一个类似于计数器的变量(锁计数器),每当加锁时,计数器+1,解锁时,计数器-1,直到计数器为0时释放锁。
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { private ReentrantLock lock = new ReentrantLock(); public void foo() { lock.lock(); // 第一次加锁 System.out.println(Thread.currentThread().getName() + " get lock."); lock.lock(); // 第二次加锁 System.out.println(Thread.currentThread().getName() + " get lock again."); lock.unlock(); // 第一次释放锁 System.out.println(Thread.currentThread().getName() + " release lock."); lock.unlock(); // 第二次释放锁 System.out.println(Thread.currentThread().getName() + " release lock again."); } public static void main(String[] args) { ReentrantLockDemo demo = new ReentrantLockDemo(); Thread t1 = new Thread(() -> { demo.foo(); }, "Thread-1"); Thread t2 = new Thread(() -> { demo.foo(); }, "Thread-2"); t1.start(); t2.start(); } } //输出结果 //Thread-1 get lock. //Thread-1 get lock again. //Thread-1 release lock. //Thread-1 release lock again. //Thread-2 get lock. //Thread-2 get lock again. //Thread-2 release lock. //Thread-2 release lock again.
公平锁和非公平锁:公平锁是指多个线程在等待锁时,按照等待的时间先后依次获得锁,即先到先得的策略。非公平锁则是不考虑等待的时间先后,直接去抢占锁,这可能会导致某些线程一直无法获得锁。
支持中断响应。可重入锁允许在等待锁的过程中响应中断。
支持多条件变量。可重入锁可以为每个条件变量创建一个等待队列,并且可以在条件变量上等待或者唤醒指定数量的线程。
优点:可以重复获取锁避免死锁,性能好,可扩展(公平锁非公平锁、可重入读写锁等)
缺点:代码复杂
优点:保证操作的完整性,不需要加锁,保证数据的一致性和可见性
缺点:不能保证并发访问的顺序,不能保证数据的原子性(如果操作的数据比较大,仍然需要加锁来保证原子性),实现比较复杂
Vector:可以理解为线程安全的ArrayList,提供的方法与ArrayList相似,使用synchronized关键字实现。比较古老,可以类比HashTable和HashMap的关系。
CopyOnWriteArrayList:线程安全的ArrayList,将原有的数组复制一份,然后在新数组上进行修改操作,最后再赋给原的数组引用。相对Vector更加高效。
原子操作类:包括AtomicInteger、AtomicLong、AtomicBoolean在内7种。
需要针对具体情况选择合适的线程安全技术和方法,保证多个线程能够正确、高效地访问共享资源。
不同的线程安全实现方法有不同的适用场景和性能表现。比如,synchronized关键字适用于对临界区进行加锁,可以保证线程安全,但是性能可能会受到影响;而使用ConcurrentHashMap等线程安全的集合类,则可以在高并发情况下提高性能和并发性能。
| 粒度| 性能| 使用难易
—|—|—|—
synchronized| 修饰方法或代码块,作用对象是整个类或者整个方法、类中的某个成员变量或者代码块| 性能较差,不适合高并发场景。|
只需在需要同步的方法或代码块前加上synchronized关键字
volatile| 修饰变量,作用对象是变量| 频繁读写时开销大| 只需要在变量前加上volatile关键字
ReentrantLock可重入锁| 作用对象是某个变量或者某个代码块| 性能较好,适合高并发场景。| 需要自己手动加锁和释放锁
原子操作| 作用对象是某个变量或者某个代码块| 相对较低| 实现比较复杂,需要对硬件平台和操作系统进行深入了解,对开发人员的要求比较高
区别 | HashMap | Hashtable |
---|---|---|
线程安全性 | 不安全,需要进行额外的同步操作 | 安全 |
null值 | 允许key和value都为null | 不允许 |
初始容量和扩容机制 | 默认初始容量为16,扩容机制是元素数量大于负载因子和数组长度的乘积时,将数组长度翻倍 | |
默认初始容量为11,扩容机制是元素数量大于数组长度时,将数组长度翻倍再加1 | ||
遍历方式 | 通过Iterator实现 | 通过Enumeration实现 |
HashTable公共方法源码:
// 判断Hashtable中是否存在某个value public synchronized boolean contains(Object value) { if (value == null) { throw new NullPointerException(); } Entry<?,?> tab[] = table; for (int i = tab.length ; i-- > 0 ;) { for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) { if (e.value.equals(value)) { return true; } } } return false; }
HashTable的key和value不允许为null:对于 Hashtable,插入元素时,如果该桶中已经存在元素,则通过 equals方法比较新插入的
key 和桶中已经存在的 key 是否相等,在比较 key 是否相等时需要调用 key 的 equals
方法,如果key为null则没有equals方法。Hashtable的值不允许为null,是因为在Hashtable内部,值被存储在一个Object类型的数组中,数组中的每个元素是一个单独的值对象,而不是一个值的引用。因此,如果允许值为null,将导致无法区分数组中的空槽和实际存储了null值的槽。这可能会导致在对Hashtable进行操作时出现意外的结果。为了避免这种情况,Hashtable不允许null值。
HashMap的key和value允许为null:HashMap的设计目标是尽可能提供高效的查找、插入和删除操作,因此对值的类型没有限制。这样可以让使用者在需要时自由地将null作为值来使用,增加了灵活性。在HashMap中,如果key为null,则它的哈希值为0,因此会将其放在哈希表的第0个位置。
ConcurrentHashMap与HashTable的区别:Hashtable 是基于 synchronized 实现线程安全,
ConcurrentHashMap 使用了分段锁的方式来实现线程安全。如果需要在多线程环境下使用哈希表,推荐使用 ConcurrentHashMap。
使用Collections.synchronizedMap实现线程安全代码示例:
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
synchronizedMap.put("key1", "value1");
synchronizedMap.put("key2", "value2");
String value = synchronizedMap.get("key1");
System.out.println(value);
在多线程环境下,推荐使用ConcurrentHashMap,因为它的并发度更高,性能更优,但需要注意一些细节问题,如在遍历时需要使用迭代器等。而如果是简单的线程安全需求,可以考虑使用Collections.synchronizedMap。
学习网络安全技术的方法无非三种:
第一种是报网络安全专业,现在叫网络空间安全专业,主要专业课程:程序设计、计算机组成原理原理、数据结构、操作系统原理、数据库系统、 计算机网络、人工智能、自然语言处理、社会计算、网络安全法律法规、网络安全、内容安全、数字取证、机器学习,多媒体技术,信息检索、舆情分析等。
第二种是自学,就是在网上找资源、找教程,或者是想办法认识一-些大佬,抱紧大腿,不过这种方法很耗时间,而且学习没有规划,可能很长一段时间感觉自己没有进步,容易劝退。
如果你对网络安全入门感兴趣,那么你需要的话可以点击这里
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。