多线程同步安全机制

发布于:2025-09-04 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

以性能换安全 

1.synchronized 同步

(1)不同的对象竞争同一个资源(锁得住)

(2)不同的对象竞争不同的资源(锁不住)

(3)单例模式加锁

synchronized 同步块

2.JUC并发包

明锁机制

3.volatile关键字

可见性:

以空间(内存)换安全

1.线程本地变量:ThreadLocal


以性能换安全 

1.synchronized 同步

        synchronized 同步,也叫做暗锁,自动释放锁。

        锁得住的条件是:同一个对象(同一个资源)

        如果synchronized 修饰方法,就是给这个方法加锁。这时候如果有个线程被JVM调度执行,只有等这个线程执行完毕以后,这个线程才会自动释放锁,其它的线程才有机会执行。

(1)不同的对象竞争同一个资源(锁得住)

两个线程对象t1和t2竞争同一个UserRunnable对象

如下面的代码创建了两个线程,run()方法被synchronized修饰,只有等一个线程执行完run()方法并自动释放锁,另一个线程才能执行:

package com.sync;

public class UserRunnable   implements  Runnable{

	@Override
	public  synchronized void run() {
		// TODO Auto-generated method stub
		
		for(int i=0;i<=10;i++)
		{
			System.out.println(Thread.currentThread().getName()+",执行"+i);
		}
		
	}// 自动释放锁

}

package com.sync;

public class Test {
	
	public static void main(String[] args) {
		
		UserRunnable  ur  = new UserRunnable();  // 只有一个Runnable对象
		Thread  t1 =  new Thread(ur);
		Thread  t2 =  new Thread(ur);
		
		t1.start();
		t2.start();
	}

}

运行结果:

(2)不同的对象竞争不同的资源(锁不住)

两个线程使用不同的UserThread对象,锁的是不同的对象

         两个线程使用不同的锁,没有竞争关系,所以无法实现同步机制。

package com.sync1;

public class UserThread extends Thread{
	
	//synchronized是一个加锁操作
	//synchronized首先是对同一个资源(同一个对象)的加锁
	//锁得住的条件是:同一个对象
	public  synchronized void run()
	{
		for(int i=0;i<=10;i++)
		{
			System.out.println(Thread.currentThread().getName()+",执行"+i);
		}
		
	}

}

package com.sync1;

public class Test {
	
	public static void main(String[] args) {
		
		UserThread  u1  = new UserThread();
		UserThread  u2  = new UserThread();
		
		u1.start();
		u2.start();
	}

}

运行结果:

解决方法:

package com.sync1;

public class Test {
	
	public static void main(String[] args) {
		
//		UserThread  u1  = new UserThread();
//		UserThread  u2  = new UserThread();
//		
//		u1.start();
//		u2.start();
		
		UserThread  u1  = new UserThread();
		
		Thread  t1  = new Thread(u1);
		Thread  t2 = new Thread(u1);
		
		
		t1.start();
		t2.start();
	}

}

(3)单例模式加锁

        使用synchronized给getInstance()方法加锁后,一个线程进入后立刻锁住,对象new完后自动解锁,此时对象已经创建完成,别的线程进入不用重新创建对象,所以两个线程返回的地址一样。

package com.sync2;

public class User {
	
	private static  User  u ;
	
	
	private User()
	{
		
	}
	
	public synchronized static User  getInstance()
	{
		if(null == u)
		{
			System.out.println(Thread.currentThread().getName()+"创建对象");
			u =  new User();
		}
		return u;
	}

}
package com.sync2;

public class UserThread1   extends Thread{
	
	public  void  run()
	{
		User  u1 =  User.getInstance();
		System.out.println(u1);
	}

}

package com.sync2;

public class UserThread2 extends Thread{
	
	public  void  run()
	{
		User  u2 =  User.getInstance();
		System.out.println(u2);
	}

}
package com.sync2;

public class Test {
	
	public static void main(String[] args) {
		
		UserThread1  u1 =  new UserThread1();
		UserThread2   u2 =  new UserThread2();
		
		u1.start();
		u2.start();
	}

}

运行结果:

synchronized 同步块

synchronized(),()里面一定是引用类型对象,必须是同一个对象。

对需要竞争的代码进行锁定,降低锁定的范围,优化性能。

        下面是一个多线程银行账户操作模拟系统,包含:

  • 1个银行账户(Bank)
  • 3个支付平台线程(支付宝、微信、京东)
  • 使用同步机制保证账户操作的线程安全

1. Bank 类(共享资源)

package com.sync3;

//银行类
public class Bank {

	// 卡号
	private String bankNumber = "";

	// 账户的金额
	private double money = 0.0;

	public Bank(String bankNumber, double money) {
		this.bankNumber = bankNumber;
		this.money = money;

	}
	
	//操作银行账户的方法 synchronized 修饰方法,会给整个方法加锁,导致整个方法被锁定,导致锁定的范围过大
	//synchronized 同步块,对需要竞争的代码进行锁定,降低锁定的范围,优化性能
	public   void  operatorBank(double  operatorMoney)
	{
		System.out.println("欢迎您到银行办理具体的业务");
		
		synchronized(Bank.class)// ()里面一定是引用类型对象,必须是同一个对象
		{
			this.money += operatorMoney;
			
			System.out.println(Thread.currentThread().getName()+",操作的金额是:"+operatorMoney
					+",现在账户剩余的金额是:"+this.money);
		}
		
		System.out.println("谢谢您,欢迎下次光临");
	}

}

2. 支付线程类

package com.sync3;

public class JindongThread   extends Thread{
	
	double opMoney;
	Bank bank;
	
	public  JindongThread(String  threadName,double opMoney,Bank bank)
	{
		super(threadName);
		this.opMoney = opMoney;
		this.bank  = bank;
	}
	
	public void run()
	{
		this.bank.operatorBank(this.opMoney);
	}
}

package com.sync3;

public class WeixinThread  extends Thread{
	
	double opMoney;
	Bank bank;
	
	public  WeixinThread(String  threadName,double opMoney,Bank bank)
	{
		super(threadName);
		this.opMoney = opMoney;
		this.bank  = bank;
	}
	
	public void run()
	{
		this.bank.operatorBank(this.opMoney);
	}

}

package com.sync3;

public class ZhifubaoThread   extends Thread{
	
	double opMoney;
	Bank bank;
	
	public  ZhifubaoThread(String  threadName,double opMoney,Bank bank)
	{
		super(threadName);
		this.opMoney = opMoney;
		this.bank  = bank;
	}
	
	public void run()
	{
		this.bank.operatorBank(this.opMoney);
	}

}

3.Test类

package com.sync3;

/**
 * synchronized同步,就是加锁的操作,保证多线程竞争同一个资源时的安全。
 */
public class Test {

	public static void main(String[] args) {

		Bank bank = new Bank("10086", 1000.0);

		ZhifubaoThread z = new ZhifubaoThread("支付宝", 300, bank);
		WeixinThread w = new WeixinThread("微信", -400, bank);
		JindongThread j = new JindongThread("京东", 600, bank);

		z.start();
		w.start();
		j.start();
	}

}

运行结果:

2.JUC并发包

        JUC 是 java.util.concurrent 包及其子包(如 java.util.concurrent.atomic 和 java.util.concurrent.locks)的非官方但广为流传的缩写,全称是 Java Util Concurrent。它是 Java 标准库中为并发编程提供强大、高性能、线程安全的工具类的核心包。

明锁机制

  • 公平锁 (Fair Lock):new ReentrantLock(true);

    • 原则:遵循“先来后到”的公平原则。

    • 行为:当锁被释放时,会优先分配给等待时间最长的线程。就像现实中排队一样,先来的先获得服务。

    • 优点:所有线程都能得到执行机会,不会产生“饥饿”现象。

    • 缺点:性能开销较大。因为需要维护一个有序队列来管理线程,上下文切换更频繁。

  • 非公平锁 (Non-fair Lock):new ReentrantLock(false); 

    • 原则:允许“插队”。

    • 行为:当锁被释放时,所有正在尝试获取锁的线程(包括刚来和已经等待的)都会去竞争,谁抢到就是谁的。如果没抢到,才会被加入到等待队列的末尾。

    • 优点吞吐量高,性能更好。减少了线程切换的开销,充分利用了CPU时间片。

    • 缺点:可能导致某些线程长时间等待,永远拿不到锁(饥饿)。

package com.demo2;

import java.util.concurrent.locks.Lock;

public class Buy implements Runnable {

	Lock lock;

	private boolean flag = true;

	private int sum = 10;

	public Buy(Lock lock) {
		this.lock = lock;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (flag) {
			
			lock.lock(); // 明锁
			try {
				Thread.sleep(1000);

				System.out.println(Thread.currentThread().getName() + ",买到了票,是第" + this.sum-- + "张票");

				
				if (this.sum <= 1) {
					this.flag = false;
				}
				

				lock.unlock(); // 一定要手动释放锁

			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}

package com.demo2;

//JUC并发包
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test {
	
	public static void main(String[] args) {
		
		
		//明锁机制   
		//new ReentrantLock(true); 是true是公平锁,公平的意思就是大家都有机会执行
		//new ReentrantLock(false); 是false是非公平锁,公非平的意思就是会有一个线程独占执行
		Lock  lock  =  new ReentrantLock(true);
		
		Buy  buy   = new Buy(lock);
		
		new Thread(buy,"张三线程").start();
		new Thread(buy,"李四线程").start();
		new Thread(buy,"王五线程").start();
		
	}

}

公平锁运行结果:

非公平锁运行结果:

3.volatile关键字

  volatile是Java提供的一种轻量级的同步机制,用于确保变量的可见性和一致性。

可见性:
  • 当一个线程修改了volatile变量时,新值会立即被刷新到主内存

  • 其他线程读取该变量时,会强制从主内存重新读取最新值

  • 解决了线程间数据不可见的问题

不保证原子性:

  • volatile不能保证复合操作的原子性

  • 比如count++这样的操作(读取-修改-写入)不是原子性的

下面这段代码展示了volatile最经典的用法——作为状态标志位:

package com.volatiledemo;

//volatile不能保证非原子操作的可见性和一致性
public class Test {
	
	public static void main(String[] args) {
		UserThread u =  new UserThread();
		u.start();

		
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		u.setFlag(false);
		
		
	}

}
package com.volatiledemo;

public class UserThread   extends Thread{
	
	// 当flag声明为volatile时,主线程修改flag = false后,UserThread立即能看到这个变化
	private volatile boolean  flag  =true;
	private int a = 0;
	
	//private  boolean  flag  =true;
	public  void  run()
	{
		System.out.println(Thread.currentThread().getName()+",线程开始运行");
		
		while(flag)
		{
			a =10;
			a++;
		}
		
		System.out.println(Thread.currentThread().getName()+",线程结束运行"+"a的值为:"+a);
	}

	public boolean isFlag() {
		return flag;
	}

	public void setFlag(boolean flag) {
		this.flag = flag;
	}

}

运行结果:

以空间(内存)换安全

1.线程本地变量:ThreadLocal

        ThreadLocal是Java提供的线程局部变量,它为每个使用该变量的线程提供独立的变量副本,实现了线程间的数据隔离。

工作原理:

  • ThreadLocal内部使用ThreadLocalMap存储数据
  • 以当前线程作为key来存储和检索值
  • 每个线程都有自己独立的ThreadLocalMap

下面代码中虽然四个线程共享同一个UserRunnable实例,但由于使用了ThreadLocal:

  • 每个线程都有自己独立的User对象

  • 茉莉1线程设置的年龄不会影响栀子1线程的年龄值

  • 各线程的年龄值保持独立,互不干扰

package com.threadlocal;

import java.util.Random;

public class UserRunnable implements Runnable {

	// 线程本地变量  key-value
	ThreadLocal<User> userLocal = new ThreadLocal<User>();

	private User getUser() {
		// key:对象hascode()   value:对应这个对象   
		// 首先尝试从ThreadLocal获取User对象,每个User对象就是一个键值对
		User u = userLocal.get();

		// 如果不存在则创建新的User对象并存入ThreadLocal,确保每个线程有自己独立的User实例
		if (null == u) {
			u = new User();
			System.out.println(u);
			userLocal.set(u);
		}
		return u;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName() + ",执行run方法");

		Random r = new Random();
		int age = r.nextInt(100);

		System.out.println(Thread.currentThread().getName() + ",产生的随机数的年龄为:" + age);

		// 从ThreadLocal获取当前线程的User对象
		User u = this.getUser();
		u.setAge(age);
		
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		
		System.out.println(Thread.currentThread().getName() + ",before设置年龄的值为:" + u.getAge());

		try {
			Thread.sleep(2000);

			System.out.println(Thread.currentThread().getName() + ",after设置年龄的值为:" + u.getAge());

		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	public static void main(String[] args) {
		
		UserRunnable u = new UserRunnable();

		Thread s1 = new Thread(u, "茉莉1");
		Thread s2 = new Thread(u, "栀子1");
		Thread s3 = new Thread(u, "茉莉2");
		Thread s4 = new Thread(u, "栀子2");

		s1.start();
		s2.start();
		s3.start();
		s4.start();

	}

}

package com.threadlocal;

public class User {
	
	
	private  int age;

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
}

运行结果:


网站公告

今日签到

点亮在社区的每一天
去签到