一、简介:
当多个线程共享数据时,由于CPU负责线程的调试,所以程序无法精确地控制多线程的交替次序。如果没有特殊控制,则多线程对共享数据的修改和访问将导致数据的不一致。
二、为什么需要线程同步:
上篇学习的线程都是独立且异步运行的,也就是说每个线程都包含了运行时所需要的数据或方法,不必关心其他线程的状态和行为。但是经常会有一些同时运行的线程需要操作共同数据,此时就要考虑其他线程的状态和行为;否则,不能保证程序运行结果的正确。
三、线程同步解决的问题:
线程同步是多线程编程中的一个重要概念,它主要解决以下几个问题:
-
数据一致性:在多线程环境中,如果多个线程同时访问和修改共享数据,可能会导致数据的不一致性。线程同步确保在任何时刻,只有一个线程能够修改共享数据。
-
避免竞态条件:竞态条件是指多个线程的执行顺序影响程序结果的情况。线程同步可以控制线程的执行顺序,防止竞态条件的发生。
-
提高效率:通过合理的线程同步,可以减少线程间的不必要等待,提高程序的执行效率。
-
资源分配:在多线程程序中,某些资源(如文件、数据库连接等)是有限的。线程同步可以确保这些资源被合理分配,避免资源冲突。
-
互斥:在访问某些需要互斥操作的资源时,线程同步可以保证在执行这些操作时,不会有其他线程同时访问。
-
协调线程:线程同步可以协调线程之间的工作,例如,通过同步机制,可以让一个线程等待另一个线程完成特定的任务后再继续执行。
四、实现线程同步:
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这称为线程同步。线程同步相当于为线程中需要一次性完成不允许中断的操作加上一把锁,从而解决冲突。
方式有两种:
(1)添加同步代码块:
代码块即使用"{}"括起来的一段代码,使用synchronized关键字修饰的代码块被称为同步代码块。其语法如下。
synchronized(obj){
//需要同步的代码
}
如果一个代码块带有synchronized(obj)标记,那么当线程执行代码时,必须先获得obj变量所引用的对象的锁,其可针对任何代码块,并且可以任意指定上锁的对象,因此灵活性更高。
(2)添加同步方法:
如果一个方法的的代码都属于需同步的代码,那么这个方法定义处可以直接使用synchronized关键字修饰,即同步方法。其语法如下。
访问修饰符 synchronized 返回类型 方法名(参数列表) {//省略方法体……}
或
synchronized访问修饰符 返回类型 方法名(参数列表) {//省略方法体……}
五、在java中有哪些线程同步机制:
在Java中,常用的线程同步机制包括以下几种:
-
synchronized关键字:
- 可以用于方法或代码块,确保同一时间只有一个线程可以执行该段代码。
- 可以用于对象锁(任意对象实例)和类锁(使用
ClassName.class
)。
-
Lock接口及其实现类:
- Java并发API提供了
Lock
接口,如ReentrantLock
,提供了比synchronized
更丰富的锁操作,如尝试非阻塞获取锁、可中断的锁获取等
- Java并发API提供了
-
wait/notify机制:
- 与
synchronized
结合使用,允许线程在某些条件不满足时挂起,并在条件满足时被其他线程唤醒。
- 与
这些同步机制可以单独使用,也可以组合使用,以满足不同的线程同步需求。正确使用这些机制对于避免并发问题和提高程序性能至关重要。
六、使用synchronized关键字实现线程同步机制的步骤:
使用 synchronized
关键字实现同步机制通常遵循以下步骤:
-
确定锁对象:
- 首先需要确定一个对象作为锁对象(Lock Object),这个对象将用于同步线程的访问。通常可以使用当前实例对象
this
,或者定义一个专门的锁对象。
- 首先需要确定一个对象作为锁对象(Lock Object),这个对象将用于同步线程的访问。通常可以使用当前实例对象
-
同步方法:
- 你可以将整个方法声明为同步的,通过在方法声明前加上
synchronized
关键字。这样,任何时候只有一个线程可以执行该对象的所有同步实例方法。
- 你可以将整个方法声明为同步的,通过在方法声明前加上
-
public synchronized void myMethod() { // 方法体 }
-
同步代码块:
- 如果只需要同步方法中的部分代码,可以使用同步代码块。在需要同步的代码前使用
synchronized
关键字,并指定锁对象。 public void myMethod() { synchronized (this) { // 需要同步的代码 } }
- 如果只需要同步方法中的部分代码,可以使用同步代码块。在需要同步的代码前使用
-
访问共享资源:
- 在同步的方法或代码块中,可以安全地访问和修改共享资源,因为此时只有一个线程可以执行这部分代码。
-
避免死锁:
- 使用
synchronized
时要注意避免死锁的发生。确保按照一致的顺序获取锁,或者使用超时机制尝试获取锁。
- 使用
-
最小化同步区域:
- 尽量缩小同步代码块的范围,只对需要同步的代码进行同步,以减少线程阻塞的时间,提高程序的并发性能。
-
释放锁:
- 当同步代码块执行完毕后,锁会自动释放,允许其他线程进入同步区域。如果同步是通过方法实现的,方法执行完毕后锁也会被释放。
-
测试和调试:
- 同步代码可能会引入并发问题,因此需要进行充分的测试和调试,确保程序在多线程环境下的正确性和性能。
七、使用synchronized的实例测试代码:
测试代码一:
通过实现runnable接口,实现同步:
public class Thread8 implements Runnable{
private int count=0;
@Override
public void run() {
while (true){
synchronized (this){
if (count<=100){
System.out.println(Thread.currentThread().getName()+":"+count++);
notify();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
Thread8 thread8 = new Thread8();
Thread thread1 = new Thread(thread8,"thread1");
Thread thread2 = new Thread(thread8,"thread2");
thread1.start();
thread2.start();
}
}
测试代码二:
对上述方法的一个实例应用
public class Thread9 implements Runnable {
private int ticket=100;
Object obj=new Object();
@Override
public void run() {
while (true){
synchronized (obj){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+":"+"卖"+ticket);
ticket--;
}else{
System.out.println("票卖完了");
ticket--;
break;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread9 thread9 = new Thread9();
Thread thread1 = new Thread(thread9);
Thread thread2 = new Thread(thread9);
Thread thread3 = new Thread(thread9);
thread1.start();thread2.start();
thread3.start();
}
}
测试代码三:
使用同步代码块的一个实例:(卖包子)
继承Thread类来实现,同步代码块控制的实际是包子
创建一个包:建立三个java类和一个test类:
第一个java类代码:
第二个java类代码:
public class Baozipu extends Thread{
private BaoZi bz;
public Baozipu(BaoZi bz){
this.bz=bz;
}
//设置线程任务,生产包子
//重写run方法
@Override
public void run() {
int count=0;
while (true){
//同步代码块:
synchronized (bz){
if (bz.flag==true){
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//完成制作包子
if (count %2==0){
bz.pi="薄皮";
bz.xian="三鲜";
}else {
bz.pi="冰皮";
bz.xian="牛肉";
}
count++;
System.out.println("正在生产:"+bz.pi+bz.xian++"包子"+"吃货正在等待中");
//生产时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bz.flag=true;
bz.notify();//唤醒线程
System.out.println("包子店的包子生产好啦:"+bz.pi+bz.xian+"吃货可以开始吃啦");
}
}
}
}
第三个java类代码:
package Thread;
public class ChiHuo extends Thread{
private BaoZi bz;
public ChiHuo(BaoZi bz){
this.bz=bz;
}
@Override
public void run() {
//吃包子
while (true){
synchronized (bz){
if (bz.flag==false){
try {
bz.wait();//等待生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃:"+bz.pi+bz.xian+"包子");
bz.flag=false;//修改包子的状态
bz.notify();//唤醒包子铺线程,生产包子
System.out.println("吃货已经吃完了:"+bz.pi+bz.xian+",包子铺生产包子中");
}
}
}
}
测试它的代码:
package Thread;
public class Test {
public static void main(String[] args) {
//创建包子对象
BaoZi baoZi = new BaoZi();
//生产包子
new Baozipu(baoZi).start();
//吃包子
new ChiHuo(baoZi).start();
}
}
八、使用Lock接口及其实现类实现同步操作:
1.获取Lock对象:
- 首先,需要创建一个
Lock
对象的实例。通常使用ReentrantLock
类。 -
Lock lock = new ReentrantLock();
2.尝试获取锁:
- 在访问共享资源之前,使用
lock.lock()
方法尝试获取锁。 -
lock.lock(); try { // 访问或修改共享资源 } finally { // 确保释放锁 lock.unlock(); }
3.使用try-finally结构:
使用 try-finally
结构确保即使在发生异常的情况下也能释放锁。
4.避免死锁:
-
避免死锁:
- 即使使用
Lock
接口,也要注意避免死锁的发生,确保锁的获取和释放顺序一致。
- 即使使用
-
测试和调试:
- 使用
Lock
接口编写的代码也需要进行充分的测试和调试,确保在多线程环境下的正确性和性能。
- 使用
九、 使用Lock接口及其实现类测试代码:
测试代码一:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LOck1 {
private final Lock lock=new ReentrantLock();
private int count=0;
public void increment(){
lock.lock();
try {
count++;
}
finally{
lock.unlock();
}
}
public int getCount(){
lock.lock();
try {
return count;
}
finally{
lock.unlock();
}
}
public static void main(String[] args) {
//通过Runnable多态指向一个工作的多线程
LOck1 lOck1 = new LOck1();
Runnable task=() ->{
for (int i = 0; i < 1000; i++) {
lOck1.increment();
System.out.println("lOck1.getCount() = " + lOck1.getCount());
}
};
//非常重要,没有的话就不能实现
task.run();
}
}
测试代码二:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Lock2 {
//创建锁对象
private Lock lock=new ReentrantLock();
//创建共享对象
private int add=0;
//处理数据的方法
public void addSue(){
lock.lock();
try {
add++;
}finally {
lock.unlock();
}
}
public int getAdd(){
return add;
}
public static void main(String[] args) {
Lock2 lock2 = new Lock2();
//使用多线程模拟并发操作,创建多个线程
for (int i = 0; i <5000 ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
lock2.addSue();
}
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程的计算结果"+lock2.getAdd());
}
}
十、以下是Lock锁的一些关键特点和简单说明:
1.显式获取和释放:使用Lock,你需要显式地调用lock()方法来获取锁,并在完成后调用unlock()方法来释放锁。这提供了比synchronized更多的灵活性,因为你可以选择在代码的任何位置获取和释放锁。
2.可中断的获取锁:Lock接口提供了lockInterruptibly()方法,允许线程在等待获取锁时被中断。这对于响应取消操作或超时等外部信号很有用。
3.尝试获取锁:通过tryLock()方法,线程可以尝试获取锁,如果锁当前不可用,则不会阻塞,而是立即返回。这可以用于非阻塞算法或当你不希望线程无限期等待时。
4.定时获取锁:tryLock(long time, TimeUnit unit)方法允许线程在指定的时间内尝试获取锁。如果在这个时间内锁变得可用,则线程会获取锁;否则,线程会放弃并返回。
5.多个锁和锁排序:与synchronized只能锁定单个对象不同,你可以使用多个Lock实例来锁定不同的对象或资源。这提供了更细粒度的并发控制,并允许你以特定的顺序获取多个锁,从而避免死锁。
6.可重入性:ReentrantLock是Lock接口的一个实现,它是可重入的,这意味着同一个线程可以多次获取同一个锁,而不会导致死锁。每次获取锁时,锁的内部计数器会增加;每次释放锁时,计数器会减少。只有当计数器为0时,其他线程才能获取该锁。
7.公平性和非公平性:ReentrantLock可以配置为公平锁或非公平锁。公平锁按照线程请求锁的顺序来授予锁,这有助于减少饥饿现象;而非公平锁则不保证这个顺序,可能会导致某些线程长时间等待。默认情况下,ReentrantLock是非公平的,但可以通过构造函数指定为公平的。
使用Lock接口和它的实现类(如ReentrantLock)可以提供更强大和灵活的并发控制机制,但也需要更多的注意和谨慎,以确保正确地获取和释放锁,避免死锁和其他并发问题。
十一、synchronized和Lock的区别:
synchronized是java关键字,而Lock是java中的一个接口
2、synchronized会自动释放锁,而Lock必须手动释放锁
3、synchronized是不可中断的,Lock可以中断也可以不中断
4、通过Lock可以知道线程有没有拿到锁,而synchronized不能
5、synchronized能锁住方法和代码块,而Lock只能锁住代码块
6、Lock可以使用读锁提高多线程读效率
7、synchronized是非公平锁,ReentranLock可以控制是否公平锁
十二、公平锁和非公平锁的区别:
公平锁:
每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
非公平锁:
每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
举个例子:公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过,
例子:ReentrantLock 同时支持两种锁:
//创建一个非公平锁,默认是非公平锁
Lock nonFairLock= new ReentrantLock();
Lock nonFairLock= new ReentrantLock(false);
//创建一个公平锁,构造传参true
Lock fairLock= new ReentrantLock(true);
适用场景:
更多的是直接使用非公平锁:非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数。
十三、乐观锁和悲观锁的区别
乐观锁和悲观锁是两种不同的并发控制策略,它们在处理数据时持有不同的态度和假设。
//1.悲观锁
态度:悲观地认为如果不严格同步线程调用,那么一定会产生异常。
机制:使用互斥锁将资源锁定,只供一个线程调用,阻塞其他线程。
适用场景:适用于写多读少的情况3。
实现:传统关系型数据库中的行锁、表锁、读锁、写锁等,以及Java中的synchronized和ReentrantLock等独占锁4。
//2.乐观锁
态度:乐观地认为别人不会同时修改数据。
机制:不会上锁,但在更新时会判断在此期间别人是否修改了数据,可以使用版本号机制和CAS算法实现
适用场景:适用于写少读多的情况,可以提高吞吐量3。
实现:Java中的java.util.concurrent.atomic包下的原子变量类使用了乐观锁的一种实现方式,即CAS4。
总结:
悲观锁通过锁定资源来避免并发冲突,适用于写多读少的情况,但会导致资源长时间被占用,降低并发性能。乐观锁则通过乐观的态度和检查机制来减少锁的使用,适用于写少读多的情况,可以提高系统的吞吐量,但可能会导致数据不一致的问题。选择哪种策略取决于具体的应用场景和需求。
十四、多线程编程之线程池(Thread Pool):
在Java多线程编程中,线程池(Thread Pool)是一种用于优化线程管理的技术。线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的ThreadFactory创建一个新线程。
(一)、线程池的主要优势在于:
1.降低资源消耗:通过重复利用已创建的线程,避免线程的频繁创建和销毁所带来的性能开销。
2.提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
3.提高系统的稳定性:由于线程数量得到有效控制,可以避免大量的线程导致系统资源耗尽的情况。
4.Java的java.util.concurrent包提供了几个用于创建线程池的工厂方法和类,如Executors.newFixedThreadPool、Executors.newCachedThreadPool、Executors.newSingleThreadExecutor等。这些工厂方法返回实现了ExecutorService接口的线程池对象,该接口定义了一些用于管理线程池的方法,如submit(提交任务)、shutdown(关闭线程池)等。
(二)、使用线程池时,开发者需要注意以下几点:
1.线程池的大小应根据实际的应用场景和需求来设定,避免过大或过小。
2.对于耗时的任务,应考虑使用异步执行,避免阻塞主线程或线程池中的其他线程。
3.需要合理处理任务执行过程中可能出现的异常,避免影响线程池的稳定性和可用性。
4.当不再需要线程池时,应调用shutdown或shutdownNow方法来关闭线程池,释放资源。
十五、下面是使用ExecutorService线程执行器的基本步骤:
1.创建线程池:使用Executors类中的静态工厂方法来创建线程池。例如,newFixedThreadPool、newCachedThreadPool或newSingleThreadExecutor。
2.提交任务:使用submit或execute方法将任务提交给线程池。submit方法返回一个Future对象,可以用于获取任务的结果;而execute方法不返回任何结果。
3.等待任务完成:如果使用了submit方法,并且需要获取任务的结果,可以使用Future对象的get方法来等待任务完成并获取结果。
4.关闭线程池:当不再需要线程池时,应调用shutdown或shutdownNow方法来关闭它。
十六、下面是一个使用ExecutorService线程执行器的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolExecutorExample {
public static void main(String[] args) throws Exception {
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交任务并获取Future对象
Future<String> future = executorService.submit(new CallableTask());
// 等待任务完成并获取结果
String result = future.get();
System.out.println("Task result: " + result);
// 关闭线程池
executorService.shutdown();
}
}
class CallableTask implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Task executed by thread: " + Thread.currentThread().getName());
// 模拟任务执行时间
Thread.sleep(2000);
return "Task result";
}
}
在这个示例中,我们创建了一个CallableTask类,它实现了Callable<String>接口。Callable接口类似于Runnable接口,但它允许任务返回一个结果。submit方法接受一个Callable对象,并返回一个Future对象,可以用来获取任务的结果。
在main方法中,我们创建了一个固定大小的线程池,并提交了一个CallableTask任务。我们使用future.get()方法等待任务完成并获取结果。最后,我们调用shutdown方法来关闭线程池。
请注意,当使用Future.get()方法时,如果任务尚未完成,调用线程将阻塞,直到任务完成。因此,在生产代码中,可能需要更复杂的逻辑来处理这种情况,例如使用超时或异步处理结果。
此外,使用线程池时还需要注意异常处理。如果任务在执行过程中抛出异常,它将被封装在ExecutionException中抛出,因此在调用future.get()时需要处理这种异常。同样,如果线程池关闭时任务尚未完成,get方法将抛出CancellationException。
十七、下面是线程池的测试代码:
测试代码一:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Example1 {
public static void main(String[] args) {
//创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(20);
//提交任务到线程池指向
for (int i = 0; i <10; i++) {
executorService.execute(new Work());
}
//关闭线程流
executorService.shutdown();
}
}
测试代码二:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Example2 {
public static void main(String[] args) {
//创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(20);
//提交任务到线程池指向
for (int i = 0; i <10 ; i++) {
final int work=i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程任务"+work+"正在运行"+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程工作任务"+work+"结束");
}
});
}
//关闭线程流
executorService.shutdown();
}
}