【面试】浅学Synchronized、Lock、Volatile、闭锁,死锁...

目录

Synchronized 和 Lock的区别?

synchronized关键字和Lock的代码举例

synchronized关键字:

Lock:

synchronized关键字实现可重入锁:

Lock实现可重入锁:

运行结果:

Lock的实现类有那些?

什么是死锁?

什么是行锁?

悲观锁和乐观锁?

乐观锁:

悲观锁:

什么是闭锁(CountDownLatch)?

Synchronized 锁升级原理

Synchronized 加非静态和静态方法上的区别

Synchronized(this) 和 Synchronized (User.class) 的区别?

ThreadLocal与Synchronized的区别?

Synchronized 和 volatitle 关键字的区别?

Volatile关键字的作用?

为什么Volatile不能保证原子性?

一个类中两个方法中都有Synchronized(this) 请问能锁住吗?为什么?

数据库中的锁有哪些?


Synchronized 和 Lock的区别?

并发操作:同一时间可能有多个事务对同一数据进行读写操作

他们都是用来解决并发编程中的线程安全问题的,不同的是

1、lock是一个接口,Lock是显式锁加锁解锁都需要我们使用java代码实现

而synchronized是java的一个关键字。是隐式锁,加锁解锁是JVM管理的

2、synchronized在发生异常时会自动释放占有的锁,因此不会出现死锁;

而lock发生异常时,不会主动释放占有的锁,必须手动来释放锁(finally中释放),可能引起死锁的发生。

3、synchronized能锁住类、方法和代码块,而Lock是块范围内

4、可重入性:Synchronized是可重入锁,意味着同一个线程可以多次获得同一个锁。而Lock也可以实现可重入性,但需要手动实现,比较灵活。

synchronized关键字和Lock的代码举例

下面是synchronized关键字和Lock的代码举例说明:

synchronized关键字:

package LockAndSynchronized;
/**
 * Synchronized关键字 解决线程安全问题
 *
 * Synchronized和Lock都是用于实现多线程同步的机制,但它们有一些区别。
 *
 * Synchronized是一个关键字,Lock是一个接口
 *
 * 可重入性:Synchronized是可重入锁,意味着同一个线程可以多次获得同一个锁。而Lock也可以实现可重入性,但需要手动实现,比较灵活。
 *
 * 锁的释放方式:Synchronized在代码块执行结束或异常时会自动释放锁,而Lock需要手动调用unlock()方法释放锁,因此需要更多的控制权。
 *
 * 锁的灵活性:Lock提供了更多的功能,例如可以实现公平锁、可中断锁、尝试获得锁等,而Synchronized只能是非公平锁,不能中断正在等待锁的线程。
 * */
public class SynchronizedExample {
    private int count = 0;


    /**
     * 未加锁
     * */
    public void increment() {
        count++;
    }
    /**
     * 锁住方法
     * */
//    public synchronized void increment() {
//        count++;
//    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            }).start();
        }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.getCount());
    }
}

Lock:

package LockAndSynchronized;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * Lock是一个接口,解决线程安全问题
 *
 * Lock需要手动调用unlock()方法释放锁,因此需要更多的控制权。
 * */
public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            //将释放锁放在finally中不管什么时候都会释放锁
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockExample example = new LockExample();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            }).start();
        }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.getCount());
    }
}

再举一个使用synchronized和lock实现可重入锁的例子

synchronized关键字实现可重入锁:

public class SynchronizedReentrantExample {
    public synchronized void outer() {
        System.out.println("Outer method");
        inner();
    }

    public synchronized void inner() {
        System.out.println("Inner method");
    }

    public static void main(String[] args) {
        SynchronizedReentrantExample example = new SynchronizedReentrantExample();
        example.outer();
    }
}

Lock实现可重入锁:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private Lock lock = new ReentrantLock();

    public void outer() {
        lock.lock();
        try {
            System.out.println("Outer method");
            inner();
        } finally {
            lock.unlock();
        }
    }

    public void inner() {
        lock.lock();
        try {
            System.out.println("Inner method");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        example.outer();
    }
}

        在这两个例子中,我们分别使用Synchronized和ReentrantLock(Lock接口的实现类)实现了一个可重入的锁。outer()方法和inner()方法都使用了相同的锁,当outer()方法内部调用inner()方法时,如果锁是可重入的,那么线程可以再次获取该锁,从而顺利执行inner()方法,而不会被阻塞。运行这两个例子,我们可以看到输出结果是一致的,说明这两种锁都实现了可重入性。

运行结果:

Outer method
Inner method

Lock的实现类有那些?

Lock本质上是一个接口,它定义了释放锁【unlock()方法】和获得锁【lock()方法】的抽象方法。

ReentrantLock(可重入锁):

可重入锁是指同一个线程在获取某个锁之后,可以再次获取该锁,而不会被阻塞。再次获取该锁而不会出现死锁指在多线程编程中,两个或多个线程相互等待对方释放资源,导致所有线程都无法继续执行的一种状态】。再次获取锁的同时会判断当前线程是否持有该锁,如果有,就对锁的次数进行加1,释放锁的时候加了几次锁,就需要释放几次锁。

ReentrantReadWriteLock类中的静态内部类ReadLock(读-写锁)

与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)。从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
写写/读写需要互斥,读读不需要互斥【因为你都在读取数据又没人改动你加啥锁嘛】。

什么是死锁?

        死锁是指在多线程编程中,两个或多个线程相互等待对方释放资源,导致所有线程都无法继续执行的一种状态。在死锁的情况下,线程之间陷入僵持状态,无法继续进行,程序可能会永远停止执行。

死锁通常发生在多个线程试图获取多个共享资源的时候,如果获取资源的顺序不当,就有可能造成死锁。典型的死锁场景包括:

互斥:线程对资源进行排它性的访问,即一次只能有一个线程占有资源。
请求与保持:线程在持有资源的同时,又请求其他资源。
不可剥夺:已经分配给线程的资源不能被其他线程强制性地释放。
环路等待:多个线程形成一个环状的等待链,每个线程都在等待下一个线程所持有的资源。

下面是一个简单的Java代码示例,用于演示死锁:

public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: Waiting for resource 2...");
                synchronized (resource2) {
                    System.out.println("Thread 1: Acquired resource 2.");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2: Waiting for resource 1...");
                synchronized (resource1) {
                    System.out.println("Thread 2: Acquired resource 1.");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

        在这个例子中,我们创建了两个线程thread1和thread2,它们都试图同时获取resource1和resource2资源,但获取资源的顺序不同。如果这两个线程同时运行,就可能出现死锁的情况。运行这个代码片段可能会导致程序停止执行,并出现死锁。

什么是行锁?

行锁指的是对数据库表中的一行数据进行加锁,是数据库管理系统中用于实现事务隔离级别(如Read Uncommitted、Read Committed、Repeatable Read和Serializable)的一种锁机制。在并发访问数据库时,多个事务可能同时访问同一张表的不同行,为了保证数据的一致性和事务的隔离性,数据库使用行锁来控制对表中行的访问(行锁的使用通常是由数据库管理系统自动管理的)。

拓展:行锁和表锁的区别?

从中文就可以看出来一个是锁的是一行数据,一个是锁的一个表。区别在于锁的粒度不同,锁的粒度越小越好。

悲观锁和乐观锁?

悲观锁和乐观锁是两种思想,用于解决线程并发场景下资源竞争的问题

  • 悲观锁,每次操作数据的时候就会进行上锁,都会认为会出现线程并发问题(其他线程会同时对数据进行修改),执行完成过后才会释放锁,线程安全,性能较低,适用于写多读少的场景

  • 乐观锁,每次操作数据的时候,认为不会出现线程并发问题不会使用加锁的形式而是在提交更新数据的时候,判断一下在操作期间有没有其他线程修改了这个数据,如果有,则重新读取,再次尝试更新或者事务回滚,循环上述步骤直到更新成功 。否则就执行操作。线程不安全,通过减少锁的使用提高执行效率,适用于读多写少的场景

悲观锁一般用于并发冲突概率大,对数据安全要求高的场景。【会进行加锁,不会出现线程安全问题。乐观锁在执行更新时频繁失败,需要不断重试或者回滚事务,反而会浪费CPU资源

乐观锁一般用于高并发,多读少写的场景【乐观锁不会进行加锁,其他线程可以进行同时访问提高响应效率

下面对乐观锁和悲观锁进行代码举例说明:

乐观锁:

        乐观锁通常通过版本号(Version Number)或时间戳(Timestamp)来实现,每个数据记录都包含一个版本号或时间戳,当事务读取数据时,会将版本号或时间戳一同读取出来。在事务提交时,会将更新后的数据与原始数据的版本号或时间戳进行比较,如果一致,则提交成功,表示没有发生冲突;如果不一致,则提交失败,需要重新尝试。

class Account {
    private int id;
    private String name;
    private int balance;
    private int version; // 版本号

    // constructor, getters, setters, etc.
}

public class OptimisticLockingExample {
    public void updateAccount(Account account, int amount) {
        // 模拟并发情况下的读取数据和更新操作
        Account original = getAccountFromDatabase(account.getId());

        // 检查版本号是否一致
        if (account.getVersion() == original.getVersion()) {
            // 更新数据
            account.setBalance(account.getBalance() + amount);
            account.setVersion(account.getVersion() + 1); // 更新版本号
            saveAccountToDatabase(account);
        } else {
            // 版本号不一致,处理冲突
            System.out.println("Optimistic Locking: Conflict detected. Retry or rollback.");
        }
    }

    // 模拟从数据库读取数据的方法
    private Account getAccountFromDatabase(int id) {
        // ... 省略从数据库读取数据的逻辑 ...
    }

    // 模拟保存数据到数据库的方法
    private void saveAccountToDatabase(Account account) {
        // ... 省略保存数据到数据库的逻辑 ...
    }

    public static void main(String[] args) {
        OptimisticLockingExample example = new OptimisticLockingExample();
        Account account = new Account(1, "Alice", 1000, 1); // 假设数据库中版本号为1
        example.updateAccount(account, 100);
    }
}

        在这个示例中,Account类包含一个版本号字段version,在updateAccount方法中进行更新操作时,先从数据库读取原始数据,并将版本号与更新前的数据进行比较,如果一致,则进行更新,并增加版本号,最后保存到数据库。如果版本号不一致,表示数据已经被其他事务修改过,这时需要处理冲突。

悲观锁:

        在数据库中,悲观锁通常通过数据库管理系统提供的锁机制实现,如使用SELECT FOR UPDATE语句或行级锁。在Java中,悲观锁的实现也可以通过synchronized关键字或Lock接口来实现。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PessimisticLockingExample {
    private int balance = 1000;
    private Lock lock = new ReentrantLock();

    public void withdraw(int amount) {
        lock.lock(); // 加悲观锁
        try {
            if (balance >= amount) {
                balance -= amount;
                System.out.println("Withdraw " + amount + " successfully. Remaining balance: " + balance);
            } else {
                System.out.println("Withdrawal failed. Insufficient balance.");
            }
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        PessimisticLockingExample example = new PessimisticLockingExample();

        // 假设有两个线程同时进行取款操作
        new Thread(() -> {
            example.withdraw(800);
        }).start();

        new Thread(() -> {
            example.withdraw(600);
        }).start();
    }
}

        在这个示例中,PessimisticLockingExample类模拟了一个银行账户的取款操作。withdraw方法使用ReentrantLock实现了悲观锁,确保在同一时间只有一个线程能够执行取款操作。当一个线程正在执行withdraw方法时,其他线程会被阻塞,直到当前线程释放锁。

        假设两个线程同时进行取款操作,由于悲观锁的存在,只有一个线程能够成功取款,另一个线程会被阻塞,并等待锁的释放。这样可以确保取款操作是线程安全的,避免并发冲突和数据不一致的问题。

什么是闭锁(CountDownLatch)?

        闭锁(CountDownLatch)是Java并发包(java.util.concurrent)提供的一种同步工具,用于控制多个线程之间的执行顺序。它允许一个或多个线程等待其他线程完成某个操作后再继续执行。

        闭锁的基本原理是,在闭锁对象创建时指定一个初始计数值,每个线程在执行完某个操作后都会调用闭锁对象的countDown()方法,将计数值减1。同时,其他线程可以通过await()方法来等待计数值变为0,一旦计数值变为0,等待的线程将被唤醒,继续执行后续操作

闭锁的一个典型应用场景是:主线程等待所有子线程完成某个任务后再继续执行。

通过一个简单的示例来说明闭锁的用法:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) {
        //初始计数值
        int numThreads = 5;
        CountDownLatch latch = new CountDownLatch(numThreads);

        for (int i = 0; i < numThreads; i++) {
            new Thread(() -> {
                // 模拟子线程执行任务
                System.out.println("Thread " + Thread.currentThread().getId() + " is doing some work.");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread " + Thread.currentThread().getId() + " has finished its work.");
                latch.countDown(); // 每个线程执行完任务后减少计数值
            }).start();
        }

        try {
            latch.await(); // 主线程等待计数值变为0
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("All threads have finished their work. Main thread can continue.");
    }
}

        在这个示例中,我们创建了5个子线程,并且让每个子线程执行一些工作(这里用sleep模拟)。每个子线程执行完工作后会调用countDown()方法将计数值减1。主线程通过调用await()方法来等待计数值变为0,一旦所有子线程都完成工作,主线程将被唤醒,继续执行后续操作。

输出结果:

Thread 14 is doing some work.
Thread 13 is doing some work.
Thread 16 is doing some work.
Thread 15 is doing some work.
Thread 17 is doing some work.
Thread 14 has finished its work.
Thread 13 has finished its work.
Thread 16 has finished its work.
Thread 15 has finished its work.
Thread 17 has finished its work.
All threads have finished their work. Main thread can continue.

        通过闭锁,我们可以方便地实现多个线程之间的同步,等待所有线程完成某个任务后再继续执行后续操作。

Synchronized 锁升级原理

        锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。

        随着锁的竞争,锁可以从无锁到偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,只能升级不能降级。
1、无锁

        当一个共享资源没有被任何线程持有时,处于无锁状态。在这个状态下,所有线程都可以进入同步代码块(即执行被synchronized关键字包围的代码块)并访问共享资源。


2、偏向锁

        当一个线程第一次访问被synchronized修饰的代码块(同步代码块)时,JVM会将锁升级为偏向锁。偏向锁的目标是避免加锁操作,提高单线程访问同步块的性能。JVM会将当前线程ID记录在对象头中,并将对象头的标记位设置为偏向锁。在以后的访问中,线程只需要检查对象头的偏向锁标记,如果是当前线程,则可以直接进入同步代码块,无需再进行加锁操作


3、轻量级锁

        当有多个线程竞争访问同步代码块时,偏向锁会升级为轻量级锁。轻量级锁使用CAS(Compare and Swap)操作来尝试获取锁。当只有一个线程持有锁时,CAS操作可以快速成功。如果CAS操作失败,表示有其他线程竞争锁,此时会膨胀为重量级锁。


4、重量级锁

        当有多个线程竞争访问同步代码块,或者轻量级锁尝试失败时,锁会升级为重量级锁。重量级锁采用传统的互斥量实现,会导致线程阻塞和调度,以确保临界区的互斥访问。在重量级锁状态下,其他线程需要等待锁的释放才能进入同步代码块

       

Synchronized 加非静态和静态方法上的区别

非静态方法上的synchronized:

针对实例对象:非静态方法上的synchronized锁定的是实例对象(非static修饰的方法,对象.方法名),每个实例对象有自己的锁。

影响范围:不同的实例对象之间互不影响,即每个实例对象的synchronized方法是独立的

synchronized修饰非静态 代码举例:     

public class SynchronizedExample {
    private int count = 0;

    // 非静态方法上的synchronized锁定的是当前对象实例
    public synchronized void increment() {
        count++;
        System.out.println("Incremented count: " + count);
    }

    public static void main(String[] args) {
        SynchronizedExample instance1 = new SynchronizedExample();
        SynchronizedExample instance2 = new SynchronizedExample();

        // 创建两个线程分别对两个实例对象调用increment方法
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                instance1.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                instance2.increment();
            }
        });

        thread1.start();
        thread2.start();
    }
}

        在上面的例子中,我们定义了一个SynchronizedExample类,其中有一个非静态方法increment(),它使用synchronized关键字修饰。我们创建了两个实例对象instance1和instance2,并分别启动两个线程thread1和thread2来分别对这两个实例对象调用increment()方法。

由于synchronized锁的作用范围是实例对象级别的,所以instance1.increment()和instance2.increment()之间互不影响,它们使用的是不同的锁。因此,thread1和thread2可以并行执行,分别对instance1和instance2的count变量进行增加操作。

输出结果可能是类似以下的内容(实际输出可能会因为线程执行顺序不同而略有不同):

Incremented count: 1
Incremented count: 1
Incremented count: 2
Incremented count: 2
Incremented count: 3
Incremented count: 3
Incremented count: 4
Incremented count: 4
Incremented count: 5
Incremented count: 5

可以看到,thread1和thread2的输出互不干扰,它们分别对各自的实例对象的count变量进行了递增操作。这证明了非静态方法上的synchronized锁是独立于实例对象的。

静态方法上的synchronized:

针对类对象:静态方法上的synchronized锁定的是当前类对象(static修饰的方法,(类名.可省略)方法名),它对整个类的所有实例对象起作用。

影响范围:对于同一个类的不同实例对象,静态方法上的synchronized会相互影响,因为它们锁的是同一个类的Class对象

synchronized修饰静态 代码举例:  

public class SynchronizedExample {
    private static int count = 0;

    // 静态方法上的synchronized锁定的是类的Class对象
    public static synchronized void incrementStatic() {
        count++;
        System.out.println("Incremented static count: " + count);
    }

    // 非静态方法上的synchronized锁定的是当前对象实例
    public synchronized void incrementNonStatic() {
        count++;
        System.out.println("Incremented non-static count: " + count);
    }

    public static void main(String[] args) {
        SynchronizedExample instance1 = new SynchronizedExample();
        SynchronizedExample instance2 = new SynchronizedExample();

        // 创建两个线程分别对静态方法和非静态方法调用
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                incrementStatic();
                instance1.incrementNonStatic();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                incrementStatic();
                instance2.incrementNonStatic();
            }
        });

        thread1.start();
        thread2.start();
    }
}

         在上面的例子中,我们定义了一个SynchronizedExample类,其中有一个静态方法incrementStatic()和一个非静态方法incrementNonStatic(),它们都使用synchronized关键字修饰。

        我们创建了两个实例对象instance1和instance2,并分别启动两个线程thread1和thread2来分别对静态方法和非静态方法进行调用。

        由于synchronized锁的作用范围是类对象级别的,所以incrementStatic()和instance1.incrementNonStatic()之间共享同一个锁。因此,thread1和thread2会对静态方法和instance1的非静态方法的共享资源进行竞争。

        输出结果可能是类似以下的内容(实际输出可能会因为线程执行顺序不同而略有不同):

Incremented static count: 1
Incremented non-static count: 2
Incremented static count: 3
Incremented non-static count: 4
Incremented static count: 5
Incremented non-static count: 6
Incremented static count: 7
Incremented non-static count: 8
Incremented static count: 9
Incremented non-static count: 10

        可以看到,thread1和thread2的输出会相互交错,因为它们共享了静态方法的锁。而instance1.incrementNonStatic()和instance2.incrementNonStatic()之间没有竞争,因为它们分别使用了不同的实例对象的锁。这证明了静态方法和非静态方法上的synchronized锁是不同的,它们的作用范围和影响范围也是不同的。       

Synchronized(this) 和 Synchronized (User.class) 的区别?

        synchronized(this)和synchronized(User.class)的区别在于它们锁定的范围不同。前者锁定的是当前实例对象,后者锁定的是类的Class对象

ThreadLocal与Synchronized的区别?

ThreadLocal和Synchonized都是用于多线程并发访问,解决线程安全问题的

但是ThreadLocal与synchronized有本质的区别:

1、Synchronized用于线程间的数据共享而ThreadLocal则用于线程间的数据隔离

2、Synchronized是利用锁的机制,使变量或代码块在同一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得多个线程在同一时间访问到的共享数据互不影响,这样就隔离了多个线程对数据的数据共享。

一句话理解ThreadLocal,Threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

史上最全ThreadLocal 详解(二)_倔强的不服的博客-CSDN博客_threadlocal只能存一个值吗

下面是一个使用ThreadLocal和synchronized的示例:

public class ThreadLocalVsSynchronized {
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
    private static int sharedValue = 0;

    private static synchronized void incrementSharedValue() {
        sharedValue++;
    }

    public static void main(String[] args) {
        // 使用ThreadLocal的线程
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                int value = threadLocalValue.get();
                threadLocalValue.set(value + 1);
                System.out.println("ThreadLocal value in thread 1: " + threadLocalValue.get());
            }
        });

        // 使用synchronized的线程
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (ThreadLocalVsSynchronized.class) {
                    incrementSharedValue();
                    System.out.println("Shared value in thread 2: " + sharedValue);
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

输出结果:

        在上面的例子中,我们创建了一个ThreadLocal<Integer>变量threadLocalValue和一个共享变量sharedValue。threadLocalValue用于在每个线程中保持独立的数据副本,sharedValue是一个共享变量。

        在thread1线程中,我们使用threadLocalValue来保持每个线程的独立计数。而在thread2线程中,我们使用synchronized来确保对sharedValue的访问是互斥的。

        由于ThreadLocal保持了每个线程的独立状态,所以thread1线程中的计数不会影响到其他线程。而使用synchronized的thread2线程会确保对sharedValue的访问是安全的,但是会引入锁开销和竞争。

Synchronized 和 volatitle 关键字的区别?

这两个关键字都是多线程并发访问的关键字,都是用来解决线程安全问题的。不同点主要有以下几点

Synchronized:

synchronized用于实现互斥访问,即在同一时间只有一个线程可以访问被synchronized关键字保护的代码块或方法。防止多个线程同时修改共享资源,从而保证数据的一致性。
synchronized可以用于方法,代码块,类等。


volatile:

volatile用于标记一个变量,表明该变量可能被多个线程同时访问,并且保证了变量的可见性
volatile修饰的变量的读取和写入操作都不会被重排序,保证了操作的有序性。
volatile适用于在一个线程写入变量,而其他线程读取变量的场景,但不适用于复合操作(例如递增操作)。
下面是一些Synchronized和volatile关键字的示例:

使用Synchronized保护共享资源的例子:

public class SynchronizedExample {
    private int sharedValue = 0;

    public synchronized void increment() {
        sharedValue++;
    }

    public synchronized int getSharedValue() {
        return sharedValue;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final shared value: " + example.getSharedValue());
    }
}

使用Volatile保证可见性的例子:

public class VolatileExample {
    private volatile boolean flag = false;

    public void start() {
        flag = true;
    }

    public void printMessage() {
        while (!flag) {
            // do nothing, busy waiting
        }
        System.out.println("Flag is true, message printed.");
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        Thread thread1 = new Thread(() -> {
            example.start();
        });

        Thread thread2 = new Thread(() -> {
            example.printMessage();
        });

        thread1.start();
        thread2.start();
    }
}

        在第一个例子中,我们使用synchronized来保护sharedValue的增加操作,确保多个线程不会同时修改它,从而避免了数据不一致。在第二个例子中,我们使用volatile来保证flag的可见性,使得一个线程修改了flag后,另一个线程能够立即看到修改,避免了printMessage方法的无限循环。

Volatile关键字的作用?

        一、可见性:通过Volatile来保证共享变量对多个线程是可见的。

        当一个共享变量被volatile修饰时,它会保证一个线程修改的值会立即被更新到主内存,并导致其他线程中的缓存变量失效,必须要去主内存中读取新值。

        如何保证可见性的呢?【仔细看,一定会懂。】

比如说线程A和线程B对同一个共享变量a=10进行操作,当线程A读取a的数值到工作内存中,准备进行加1,但是由于某种原因还没有进行操作就阻塞了,这时候线程B对a进行了加1,并将修改的值更行到了主内存,这时候会导致线程A的工作内存中的缓存变量a失效,所以线程A就需要去主内存中重新读取最新的值进行操作。

        这就是volatile的可见性。

        二、保证有序性【禁止指令重排序】

JMM【java memory model,即java多线程内存模型】是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,程序的执行结果不能改变

double pi = 3.14;    //A
double r = 1;        //B
double s= pi * r * r;//C

上面的语句,可以按照A->B->C执行,结果为3.14,但是也可以按照B->A->C的顺序执行,因为A、B是两句独立的语句,而C则依赖于A、B,所以A、B可以重排序,但是C却不能排到A、B的前面。JMM保证了重排序不会影响到单线程的执行,但是在多线程中却容易出问题。 比如这样的代码:

int a = 0;
bool flag = false;
 
public void write() {
    a = 2;              //1
    flag = true;        //2
}
 
public void multiply() {
    if (flag) {         //3
        int result = a * a;//4
    }
}

假如有两个线程执行上述代码段,线程1先执行write,随后线程2再执行multiply,最后result的值一定是4吗?结果不一定【因为没有保证顺序,就是Flag和 a=2交换顺序了】:

 如图所示,write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。 这时候可以为flag加上volatile关键字,禁止重排序,可以确保程序的有序性,也可以上重量级的synchronized和Lock来保证有序性,它们能保证那一块区域里的代码都是一次性执行完毕的
 

为什么Volatile不能保证原子性?

        一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增(写)这样的非原子操作,就不会保证这个变量的原子性了

【原子性指的是读取数据+修改数据,读取是能够读取最新的数据,但是不一定能够修改成功,因为有多个线程进行修改这个共享变量。】。

举个栗子

一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性【读和修改都成功】

原子性:把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。

一个类中两个方法中都有Synchronized(this) 请问能锁住吗?为什么?

都有可能。

Synchronized(this) 锁住的是当前实例对象。

当创建了两个实例对象进行调用时,就锁不住。因为这时候this指代的是两个不同的实例对象,并不是同一个锁。,

当创建一个实例对象进行调用时,就锁得住。因为这时候this指代的是当前唯一的实例对象,是同一把锁。

数据库中的锁有哪些?

        在数据库中,锁是用来控制并发访问的机制,以保证数据的一致性和完整性。不同类型的锁在不同的场景下起着不同的作用。以下是一些常见的数据库锁类型:

  1. 共享锁(Shared Lock 或读锁)

    • 共享锁允许多个事务同时获取锁并读取数据,但阻止其他事务获取排它锁。共享锁适用于读取操作,多个事务可以并发读取相同的数据而不会造成冲突。
  2. 排它锁(Exclusive Lock 或写锁)

    • 排它锁只允许一个事务获取锁并修改数据,其他事务无法同时获取相同的锁。排它锁适用于写入或修改操作,确保数据的完整性。
  3. 行级锁(Row-level Lock)

    • 行级锁是针对数据库表中的单行数据而设置的锁,允许同时有多个事务访问表中不同行的数据。它能够更细粒度地控制并发访问,但可能引起死锁问题。
  4. 表级锁(Table-level Lock)

    • 表级锁是针对整个数据库表而设置的锁,阻止其他事务对整个表进行修改。它通常用于一些特殊情况下,比如备份和维护。
  5. 间隙锁(Gap Lock)

    • 间隙锁是用来锁定范围的空隙,而不是现有的行。它用于防止其他事务在一个范围内插入新行,从而避免幻读等问题。

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mxin5

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

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

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

打赏作者

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

抵扣说明:

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

余额充值