Java多线程的理解(下)


一、线程同步的定义

线程同步是指协调多个线程的执行顺序,以确保它们在访问共享资源时不会发生冲突。在多线程编程中,如果多个线程同时访问共享资源,可能会导致不可预测的结果,例如数据损坏或程序崩溃。因此,线程同步的目的是为了避免这些问题,确保多个线程之间的协作是安全和可靠的。常见的线程同步机制包括互斥锁、信号量、条件变量等。

二、Java中线程同步的方法包括:

1、synchronized关键字

使用synchronized关键字修饰方法或代码块,可以实现对方法或代码块的互斥访问,保证同一时刻只有一个线程执行。
由于每个java对象都有一个内置锁,用synchronized修饰方法或者代码块时,内置锁会保护整个方法或代码块,要想执行这个方法或者代码块必须获得其内置锁,运行时会加上内置锁,当运行结束时,内置锁会打开。由于同步是一种高开销的工作,所以尽量减少同步的内容,只需同步代码块就可以。

1.1 修饰方法

代码演示:

public class Test1 implements Runnable {
    static int i=0;
    public synchronized void test(){
    	System.out.println("当前线程为"+i);
    	i++;
    }
    public void run(){
    	test();
    }
	public static void main(String[] args) {
        Test t1=new Test();
        for(int i=0;i<10;i++){
        	Thread t11=new Thread(t1);
            t11.start();
        }
	}
}

运行结果:
当前线程为0
当前线程为1
当前线程为2
当前线程为3
当前线程为4
当前线程为5
当前线程为6
当前线程为7
当前线程为8
当前线程为9
//从上面结果看出,使用synchroniz修饰方法会在每个线程中按顺序依次执行。

代码演示(基于上边代码稍加改动):

public class Test2 implements Runnable {
    static int i=0;
    public synchronized void test(){
    	System.out.println("当前线程为"+i);
    	i++;
    }
    public void run(){
    	test();
    }
	public static void main(String[] args) {
        for(int i=0;i<10;i++){
        	//此处有改动,写进了循环里边
        	Test t1=new Test();
        	Thread t11=new Thread(t1);
            t11.start();
        }
	}
}
运行结果:
当前线程为0
当前线程为0
当前线程为0
当前线程为3
当前线程为4
当前线程为5
当前线程为5
当前线程为7
当前线程为7
当前线程为9

只是稍微修改了一处,结果就有很大不同,为什么会这样呢?我们发现上面两个代码,一个是在开始值创建一个Runnable的对象,一个是在for循环中每次都创建一个新的对象,就是因为synchronized是不能锁住不同对象的线程的,只能锁住同一个对象的线程,也就是说锁住的是方法所属的主体对象自身。

1.2 修饰代码块

public class Test implements Runnable {
    static int i=0;
    public void test(){
    	synchronized(this){System.out.println("当前线程为"+i);
    	i++;
    	}
    }
    public void run(){
    	test();
    }
	public static void main(String[] args) {
		Test t1=new Test();
        for(int i=0;i<10;i++){
        	Thread t11=new Thread(t1);
            t11.start();
        }
	}
}
运行结果:
当前线程为0
当前线程为1
当前线程为2
当前线程为3
当前线程为4
当前线程为5
当前线程为6
当前线程为7
当前线程为8
当前线程为9

1.3 修饰静态方法

public static synchronized void anotherMethod() { 
    // do something 
} 

当 synchronized修饰的是静态方法时,它锁住的是该方法所属的类的 Class 对象,即 Class 对象锁。
当一个线程进入 synchronized 修饰的静态方法时,它会尝试获取该类的 Class 对象锁,如果获取成功就可以执行该方法,其他线程则需要等待该线程释放锁后才能继续执行。因此,synchronized 修饰的静态方法实现的是对该类的所有实例的同步。
需要注意的是,synchronized修饰的静态方法只锁住了该类的 Class 对象,而不是该类的实例对象。如果该类有多个实例对象,那么这些对象的方法可以并发执行,不会被锁住。

2、Lock接口:

2.1 Lock接口的使用示例

Lock接口是Java中用于实现锁的接口,它提供了比synchronized关键字更灵活的锁定机制。
下面是Lock接口的使用示例:
1.创建Lock对象

Lock lock = new ReentrantLock(); // 创建可重入锁对象

2.获取锁

lock.lock(); // 获取锁

3.释放锁

lock.unlock(); // 释放锁

4.使用try-finally 语句确保锁的释放

lock.lock();
try {
    // 执行需要锁定的代码
} finally {
    lock.unlock();
}

5.使用lockInterruptibly()方法可以响应中断

lock.lockInterruptibly(); // 获取锁,响应中断
try {
    // 执行需要锁定的代码
} finally {
    lock.unlock();
}

Lock接口提供了比synchronized关键字更灵活的锁定机制,它支持可重入锁、公平锁和非公平锁,并提供了更细粒度的控制。使用Lock接口可以避免死锁等问题,提高程序的并发性能。

2.2 synchronized锁与lock锁的区别

  1. 使用方式不同:synchronized是Java内置的关键字,可以直接在方法或代码块中使用,而lock是一个接口,需要通过实例化一个具体的锁对象来使用。
  2. 粒度不同:synchronized锁的粒度比较粗,它可以把一个方法或代码块作为一个整体来加锁,而lock锁的粒度比较细,它可以对代码中的某一段进行加锁。
  3. 可中断性不同:synchronized锁在等待锁的过程中是不能被中断的,而lock锁则提供了lockInterruptibly()方法,支持线程在等待锁的过程中被中断。
  4. 可公平性不同:synchronized锁是不可公平的,即不能保证等待时间最长的线程最先获取锁,而lock锁则可以通过构造函数来指定是否公平锁。
  5. 锁的释放方式不同:synchronized锁会在执行完synchronized代码块或方法后自动释放锁,而lock锁必须在最后语句块中手动释放锁,不然就会出现死锁的情况。
  6. 性能不同:synchronized锁的性能相对较低,因为它是由Java虚拟机实现的。而Lock锁的性能相对较高,因为它是由Java程序实现的,并且可以通过设置等待时间来避免死锁。
  7. 锁的获取方式不同: synchronized锁是悲观锁,即线程每次获取锁时都会进行互斥访问的检查,而lock锁则是乐观锁,它使用了CAS(Compare and Swap)算法来实现锁的获取,当线程尝试获取锁时,如果发现锁已经被其他线程占用,则通过CAS算法不断尝试获取锁,直到成功为止。

2.3 案例演示

class Threadlock implements Runnable{
    Lock lock = new ReentrantLock();
    public void run(){
        lock.lock();
        try{
            String name=Thread.currentThread().getName();
            for(int i=0 ; i<5 ;i++){
                System.out.println("线程"+name+":"+i);
            }
        }
        catch(Exception e){
        }
        finally{
            lock.unlock();
        }
    }
    public static void main(String[] args){
        Threadlock threadlock = new Threadlock();
        Thread thread1 = new Thread(threadlock,"1");
        thread1.start();
        Thread thread2 = new Thread(threadlock,"2");
        thread2.start();
    }
}
运行结果:
线程1:0
线程1:1
线程1:2
线程1:3
线程1:4
线程2:0
线程2:1
线程2:2
线程2:3
线程2:4

可以看出线程按顺序执行了,通常将要锁住的代码和方法放在try-catch中,在finally中释放锁,和synchronized一样,是对同一对象的两个线程。

3、volatile关键字:

3.1 volatile关键字的理解

前面提到的synchronized关键字锁住的是代码块,但是容易造成资源的浪费,是一种重量锁,而volatile是一种轻量锁,锁住的是变量。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
volatile关键字
上面这张图表明,不同线程在执行时,数据都是从主内存中取得的,每个线程自身都有一个工作内存。线程读入和写入数据的过程如下:
先从主内存中读取数据,放入工作内存,传递到线程中使用,在修改数据时,原路返回,经工作内存再写入到主内存中。
这样就会有问题了,当两个线程1先把修改的数据经过工作内存写回到主内存的过程中,线程2读取主内存中的数据了,这样数据就出现了不一致性,我们把这种情况叫做线程之间不可见性。
当对非 volatile 变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

使用volatile关键字修饰的变量具有如下两个特性:

  1. 可见性:当一个线程修改了一个volatile变量的值时,其他线程可以立即看到这个修改。这是因为volatile变量的值会被立即写回到主存中,并且其他线程会从主存中读取最新的值,而不是缓存中的旧值。。
  2. 禁止指令重排序优化:volatile变量的读和写操作会被插入到内存屏障(Memory Barrier)之前和之后,这样可以防止指令重排序优化,保证执行顺序与代码顺序一致。(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)
    需要注意的是,使用volatile关键字修饰变量只能保证单个volatile变量的操作具有原子性,而不能保证多个volatile变量的操作具有原子性。如果需要保证多个变量的操作具有原子性,可以使用synchronized关键字或者Atomic类。

volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行

3.2 案例演示

volatile关键字的作用:

public class VolatileExample {
    private volatile boolean flag = false;

    public void setFlag(boolean value) {
        flag = value;
    }
    public void printFlag() {
        System.out.println("Flag is " + flag);
    }
}
//在上面的代码中,flag是一个共享变量,使用了volatile关键字进行修饰。
//setFlag方法可以用来修改flag的值,printFlag方法可以用来打印flag的值。

//假设现在有两个线程A和B,A线程调用setFlag方法将flag的值修改为true,然后B线程调用printFlag方法来打印flag的值。
//如果没有使用volatile关键字修饰flag,那么B线程可能看不到A线程修改flag的值,因为不同线程之间的操作可能会存在缓存不一致的问题。
//但是使用了volatile关键字修饰后,B线程就可以立即看到A线程修改flag的值,因为volatile关键字会确保多个线程之间对共享变量的可见性。

volatile关键字无法保证原子性:

public class Test implements Runnable {
	volatile int value=0;
	volatile int count=0;
	public void run(){
		for(int i=0;i<10000;i++){
			value++;
		}
		count++;
	}
	public static void main(String[] args) {
		Test test=new Test();
		for(int i=0;i<5;i++){
			Thread t=new Thread(test);
			t.start();
		}
		while(test.count!=5);
		System.out.println(test.value);
        
	}
}
运行结果:
20695

因为value的递增不是原子操作,volatile是无法保证原子性的。我们可以假设有这种情况,当value为100时线程1执行value自增的时候,比如说进行到了加1操作后被阻塞了,线程2接着进行value自增,线程2在主存中读取value值时会发现value还是100,那么线程1和线程2执行的结果都是101,相当于两次自增后value确只增加1,这就造成了实际值比50000小。

4、wait(),notify( ) 和notifyAll( ) 方法:

4.1 方法的作用

  • wait(),使一个线程处于等待状态,并释放所持对象的锁,与sleep不同,sleep不会释放对象锁。
  • notify(),唤醒一个处于阻塞状态的线程,进入就绪态,并加锁,只能唤醒一个线程,但不能确切知道唤醒哪一个,由JVM决定,不是按优先级。其实不是对对象锁的唤醒,是告诉调用wait方法的线程可以去竞争对象锁了。wait和notify必须在synchronized代码块中调用。
  • notifyAll(),唤醒所有处于阻塞状态的线程,并不是给他们加锁,而是让他们处于竞争。

为什么wait和notify要在synchronized代码块中使用
调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁,释放锁后进入等待队列。
notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于阻塞队列的线程进入等待队列竞争锁)
Synchronized应用举例:生产者消费者模型
消费者线程需要等待直到生产者线程完成一次写入操作。生产者线程需要等待消费者线程完成一次读取操作。假设没有应用Synchronized关键字,当消费者线程执行wait操作的同时,生产线线程执行notify,生产者线程可能在等待队列中找不到消费者线程。导致消费者线程一直处于阻塞状态。那么这个模型就要失败了。所以必须要加Synchronized关键字。

4.2 代码实现

package cn.quadrant;
//使用wait与notify实现
public class ProducerConsumer {

    public static void main(String[] arg) {

        Resource resource = new Resource();
        // 生产者线程
        ProducerThread p1 = new ProducerThread(resource);
        ProducerThread p2 = new ProducerThread(resource);
        ProducerThread p3 = new ProducerThread(resource);
        // 消费者线程。测试时可以少开几个消费线程看看具体
        ConsumerThread c1 = new ConsumerThread(resource);
        ConsumerThread c2 = new ConsumerThread(resource);
        ConsumerThread c3 = new ConsumerThread(resource);

        p1.start();
        p2.start();
        p3.start();
        c1.start();
        c2.start();
        c3.start();

    }

}

// 编写资源类
class Resource {
//当前资源池数量
    private int currentSize = 0;
    //允许数量
    private int allowSize = 10;

    // 取走资源,如果当前资源大于0则可以移除(消费),移除之后唤醒生产线程。否则进入等待释放线程资源
    public synchronized void remove() {
        if (currentSize > 0) {
            currentSize--;
            System.out.println(Thread.currentThread().getName() + "消费一件资源,当前资源池有" + currentSize + "个");

            notifyAll();
        } else {

            // 没有资源 消费者进入等待状态
            try {
                System.out.println(Thread.currentThread().getName() + "当前资源过少,等待增加");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }

    public synchronized void add() {
        // 如果当前数量小于限制数量则可以增加,增加后唤醒消费者消费,否则等待消费,释放锁
        if (currentSize < allowSize) {
            currentSize++;
            System.out.println(Thread.currentThread().getName() + "生产一件资源,当前资源池有" + currentSize + "个");
            notifyAll();
        } else {
            try {
                System.out.println(Thread.currentThread().getName() + "当前资源过多,等待消费");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//消费线程
class ConsumerThread extends Thread {
    private Resource resource;

    ConsumerThread(Resource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true) {
            //避免生产消费太快测试的时候看不到打印,休眠一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //移除代表消费
            resource.remove();
        }

    }

}
//生产者线程
class ProducerThread extends Thread {
    private Resource resource;
    ProducerThread(Resource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //生产
            resource.add();
        }

    }

}

5、CountDownLatch、CyclicBarrier、Semaphore等同步工具类:

Java中的CountDownLatch、CyclicBarrier和Semaphore都是用于线程同步的工具类,可以帮助我们控制线程的执行顺序和数量,从而更好地管理并发。

5.1 CountDownLatch(倒计数器)

CountDownLatch是Java中一个非常有用的同步工具类,它可以让一个或多个线程等待其他线程完成它们的操作后再执行。在Java中,CountDownLatch类是通过一个计数器来实现的,计数器的初始值可以设置为任何整数。当某个线程调用CountDownLatch的await()方法时,它会阻塞等待计数器的值变为0;而其他线程执行完它们的操作后会通过调用CountDownLatch的countDown()方法来减少计数器的值。当计数器的值变为0时,await()方法将返回,线程得以继续执行。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        int numThreads = 5;
        CountDownLatch latch = new CountDownLatch(numThreads);

        for (int i = 0; i < numThreads; i++) {
            new Thread(new Worker(i, latch)).start();
        }

        latch.await();
        System.out.println("All threads have finished!");
    }

    static class Worker implements Runnable {
        private int id;
        private CountDownLatch latch;

        public Worker(int id, CountDownLatch latch) {
            this.id = id;
            this.latch = latch;
        }

        public void run() {
            System.out.println("Worker " + id + " is working...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Worker " + id + " has finished.");
            latch.countDown();
        }
    }
}

在这个例子中,我们创建了5个线程,并用CountDownLatch来等待它们全部执行完毕。每个线程都会休眠1秒钟,然后调用countDown()方法来减少计数器的值,最终当所有线程都执行完毕时,await()方法返回,打印出"All threads have finished!"。

5.2 CyclicBarrier(循环栅栏)

它可以让一组线程在达到某个同步点时阻塞,直到所有线程都到达了该同步点,然后再一起继续执行。CyclicBarrier的一个重要特点是可以重复使用,也就是说,当所有线程都到达同步点后,计数器会被重置,可以继续使用。

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

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // 创建一个 CyclicBarrier 实例,指定等待线程数和屏障动作
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
            System.out.println("所有线程已到达屏障点,开始执行屏障动作");
        });

        // 创建 3 个线程,模拟多个线程同时到达屏障点
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 已到达屏障点,等待其他线程");
                    cyclicBarrier.await(); // 等待其他线程到达屏障点
                    System.out.println(Thread.currentThread().getName() + " 继续执行");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

上述代码中,我们创建了一个 CyclicBarrier 实例,指定等待线程数和屏障动作。然后创建了 3 个线程,模拟多个线程同时到达屏障点,每个线程到达屏障点后等待其他线程,直到所有线程到达屏障点,才会继续执行。
在 CyclicBarrier 的构造函数中,我们指定了等待线程数为 3,也就是说,只有当 3 个线程都到达屏障点时,才会执行屏障动作。屏障动作是一个 Runnable 对象,它会在所有线程到达屏障点后执行一次。
在每个线程执行的代码中,我们调用了 CyclicBarrier 的 await() 方法,等待其他线程到达屏障点。当所有线程都到达屏障点后,await() 方法会返回,线程继续执行。
需要注意的是,CyclicBarrier 的 await() 方法可能会抛出 InterruptedException 和 BrokenBarrierException 异常,因此需要在 catch 块中处理这两个异常。

5.3 Semaphore(信号量)

Semaphore是Java中的一个并发工具类,它可以控制同时访问某个资源的线程数量,可以用于限流、资源池管理等场景。
Semaphore维护了一个许可证的集合,线程可以通过调用acquire()方法来获取许可证,如果没有许可证可用,线程就会被阻塞,直到有许可证可用为止。当线程不再需要许可证时,它可以通过调用release()方法来释放许可证。

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2); // 初始化许可数量为2

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取许可
                    System.out.println(Thread.currentThread().getName() + "----取得许可.");
                    Thread.sleep(1000);
                    semaphore.release(); // 释放许可
                    System.out.println(Thread.currentThread().getName() + "----释放许可.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "Thread " + i).start();
        }
    }
}

 **运行结果:**
Thread 0----取得许可
Thread 1----取得许可
Thread 1----释放许可
Thread 0----释放许可
Thread 2----取得许可
Thread 3----取得许可
Thread 3----释放许可
Thread 2----释放许可
Thread 4----取得许可
Thread 4----释放许可
//可以看到,只有两个线程同时执行,其他线程需要等待。

在上面的代码中,我们初始化了一个Semaphore对象,许可数量为2,然后启动了5个线程,每个线程都会获取许可并执行一段代码,然后释放许可。
由于许可数量只有2个,因此只有两个线程可以同时执行,其他线程需要等待已有的线程释放许可后才能继续执行。


总结

Java线程同步是指在多线程环境下协调不同线程对共享资源的访问,以保证线程安全和数据一致性。以下是Java线程同步的总结:

  • synchronized关键字:synchronized关键字可以用来修饰方法或代码块,实现对共享资源的互斥访问。当一个线程获得了对象锁,其他线程就无法访问被锁定的资源,直到该线程释放锁。
  • Lock接口:Lock接口提供了比synchronized更细粒度的锁控制机制,可以更加灵活地控制线程同步。Lock接口提供了lock()和unlock()方法,分别用于获取锁和释放锁。
  • synchronized和Lock的区别:synchronized是Java语言层面提供的同步机制,而Lock是基于Java API实现的同步机制。synchronized是隐式锁,不需要手动释放锁,而Lock是显式锁,需要手动释放锁。
  • volatile关键字:volatile关键字可以保证变量的可见性和有序性,但无法保证原子性。当一个变量被声明为volatile时,每个线程都会从主内存中读取该变量的最新值。
  • Atomic包:Atomic包提供了一组原子操作类,可以保证对变量的更新操作具有原子性。Atomic包中的类都采用了CAS(Compare and Swap)算法,实现了乐观锁机制,避免了线程的阻塞和唤醒,提高了性能。
  • wait()、notify()和notifyAll()方法:wait()方法使线程等待,直到其他线程调用notify()或notifyAll()方法唤醒它;notify()方法唤醒等待的线程中的一个线程;notifyAll()方法唤醒等待的所有线程。
  • ReentrantLock类:ReentrantLock是Lock接口的一个实现类,提供了可重入锁的机制。可重入锁是指同一个线程在持有锁的情况下可以再次获取锁,避免了死锁的发生。
  • Condition类:Condition类是Lock接口的扩展,提供了线程等待和唤醒的机制。Condition类的await()方法使线程等待,直到其他线程调用signal()或signalAll()方法唤醒它;signal()方法唤醒等待的一个线程;signalAll()方法唤醒等待的所有线程。

以上是Java线程同步的总结,不同的同步机制适用于不同的场景,需要根据具体情况选择合适的同步方式。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是鹏鹏哦

你的鼓励将是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值