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

public class Test 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) {// TODO Auto-generated method stubTest t1=new Test();for(int i=0;i<10;i++){Thread t11=new Thread(t1);t11.start();}}
}

结果如下:

从上面结果看出,使用synchroniz修饰方法会在每个线程中按顺序依次执行。
在使用该方法时要注意以下一点
先看代码如下:

public class Test 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) {// TODO Auto-generated method stubfor(int i=0;i<10;i++){Test t1=new Test();Thread t11=new Thread(t1);t11.start();}}
}

在看下结果:

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

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) {// TODO Auto-generated method stubTest t1=new Test();for(int i=0;i<10;i++){Thread t11=new Thread(t1);t11.start();}}
}

运行结果和第一个程序一样的,不多解释了。

3.修饰静态方法

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

对于静态方法,锁住的不是这个类的对象,也不是也不是这个类自身,而是这个类所属的java.lang.Class类型的对象

二.wait与notify

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

生产者消费者代码实现:

package p2;//使用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) {// TODO Auto-generated catch blocke.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) {// TODO Auto-generated catch blocke.printStackTrace();}}}}
//消费线程
class ConsumerThread extends Thread {private Resource resource;ConsumerThread(Resource resource) {this.resource = resource;}@Overridepublic 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;}@Overridepublic void run() {while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//生产resource.add();}}}

三.volatile关键字
前面提到的synchronized关键字锁住的是代码块,但是容易造成资源的浪费,是一种重量锁,而volatile是一种轻量锁,锁住的是变量。
我们先来看下面的代码:

boolean value=false;--------------线程2------------- public void change(){ value=true; }--------------线程1--------------public void output(){ if(value==true) System.out,println("输出成功"); }

上面的结果会输出什么呢?先执行线程2,再执行线程1,是否会输出成功呢?其实是不一定的,让我们来看下为什么:

上面这张图表明,不同线程在执行时,数据都是从主内存中取得的,每个线程自身都有一个工作内存。线程读入和写入数据的过程如下:
先从主内存中读取数据,放入工作内存,传递到线程中使用,在修改数据时,原路返回,经工作内存再写入到主内存中。
这样就会有问题了,当两个线程1先把修改的数据经过工作内存写回到主内存的过程中,线程二读取主内存中的数据了,这样数据就出现了不一致性,我们把这种情况叫做线程之间不可见性。
volatile关键字就是用来解决这种不可见问题的,它是怎么实现的呢?

使用volatile修饰的变量在被一个线程修改后,直接将数据写回到主内存,跳过了工作内存
使用volatile修饰的变量,在被线程1修改后,线程二中的该变量就被视为无效
线程2中数据无效了,在使用的时候就必须重新回主内存中读取该数据
但是在使用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) {// TODO Auto-generated method stubTest 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);}}

看下结果:32130
按理来说应该会输出50000,为什么会使那个结果呢?
原因在于volatile不能满足原子性。
原子性是指一个操作要么在执行时不被打断,要么就是不执行,原子操作只有最简单的赋值和读取。我们举例子讲解一下:

int a=1;
int b=a;
a++;

上面三个只有第一个是原子操作,第二个,先读取a的值,再赋值给b,两个原子操作叠加起来就不是原子操作,第三个,相当于a=a+1,先读取a的值,再加1,再赋值给a,也不是原子操作。
注意:long和double的变量是64位的,不满足原子操作。
了解了原子性我们想想为什么之前代码的结果不是500000。因为value的递增不是原子操作,volatile是无法保证原子性的。我们可以假设有这种情况,当value为100时线程1执行value自增的时候,比如说进行到了加1操作后被阻塞了,线程2接着进行value自增,线程2在主存中读取value值时会发现value还是100,那么线程1和线程2执行的结果都是101,相当于两次自增后value确只增加1,这就造成了实际值比500000小。

四.Lock
前面的synchronized加锁,只有加锁和释放所=锁,在JDK5后出现了新的加锁方法,使用Lock,包含比synchronized更多的加锁功能。ReentrantLock类是实现了Lock接口的锁.ReentrantLock类的常用方法:

ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁

两者区别:

synchronized是java的关键字,是java的内置特性,是基于JVM层面的,而Lock是接口,是基于javaJDK层面的,通过这个接口可以实现同步访问。
synchronized是不需要手动释放锁的,在代码执行完后,系统会让线程自动释放锁,但是Lock要手动解锁,如果不手动解锁,会出现死锁现象。

public class Test 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){ Test test = new Test();Thread thread1 = new Thread(test,"1"); thread1.start(); Thread thread2 = new Thread(test,"2");thread2.start(); } }

结果如下:

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

五.ThreadLocal类
使用ThreadLocal管理变量,每一个使用该变量的线程都获得该变量的副本,各个副本之间相互独立,每个线程都可以随意修改变量副本,而不会对其他线程造成影响。
ThreadLocal类的常用方法

ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value

来看一下如何使用:

public class Test implements Runnable{
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){protected Integer initialValue() { return 0; } };public void run(){for(int i=0 ; i<5 ;i++)value.set(value.get()+1); String name = Thread.currentThread().getName(); System.out.println("线程"+name+":"+value.get());}public static void main(String[] args){Test test = new Test(); Thread thread1 = new Thread(test,"1");thread1.start(); Thread thread2 = new Thread(test,"2");thread2.start(); } }

结果如下:

注:ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

摘选: https://tangyulu.com/233.htm

Java实现线程同步的五种方法相关推荐

  1. 实现Java线程同步的五种方法

    线程同步概念 Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不明确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的 ...

  2. java线程同步的五种方法

    2019独角兽企业重金招聘Python工程师标准>>> 1.同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, ...

  3. JAVA中线程同步的几种实现方法

    JAVA中线程同步的几种实现方法 一.synchronized同步的方法: 1.synchronized同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁, ...

  4. JAVA中创建线程池的五种方法及比较

    之前写过JAVA中创建线程的三种方法及比较.这次来说说线程池. JAVA中创建线程池主要有两类方法,一类是通过Executors工厂类提供的方法,该类提供了4种不同的线程池可供使用.另一类是通过Thr ...

  5. C#线程同步的几种方法

    在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳. 一.volatile关键字 volatile是最简单的一种同步方法,当然简单是要付出代价的.它只能在变量一级做 ...

  6. 归纳一下:C#线程同步的几种方法

    我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库或网络文件等.这些情况你都可以创建一个子线程 ...

  7. 在 Java 中初始化 List 的五种方法

    转载自  在 Java 中初始化 List 的五种方法 Java 中经常需要使用到 List,下面简单介绍几种常见的初始化方式. 1.构造 List 后使用 List.add 初始化 List< ...

  8. unix c线程同步的三种方法:互斥量、读写锁以及条件变-xhb8413-ChinaUnix博客

    unix c线程同步的三种方法:互斥量.读写锁以及条件变-xhb8413-ChinaUnix博客 unix c线程同步的三种方法:互斥量.读写锁以及条件变 2012-03-30 14:42:38 分类 ...

  9. linux:线程同步的5种方法

    linux:线程同步的5种方法 一.为什么要使用线程: 二.线程同步的5种方法 2.1 互斥量 2.2 读写锁 2.3 条件变量 2.4 自旋锁 2.5 屏障 一.为什么要使用线程: <1> ...

最新文章

  1. 逆向-攻防世界-maze
  2. Tomcat内存溢出解决方案
  3. azure devops中文显示乱码_【Azure DevOps系列】Azure DevOps生成代码覆盖率
  4. 【Linux】一步一步学Linux——useradd命令(82)
  5. 浅谈MSTP时延与带宽和速率的关系
  6. centos7 mysql数据库安装和配置
  7. Hadoop Yarn REST API未授权漏洞利用挖矿分析
  8. Linux Framebuffer驱动剖析之中的一个—软件需求
  9. shell 脚本学习及troubleshooting
  10. 【python】list,dict赋值不要用等号,要用extend,update
  11. vvv在线文档导出工具_墙裂推荐 | 在线文档编辑工具
  12. Oracle alter 语句用法
  13. MySQL查询效率问题
  14. CAD如何绘制带有弧形的箭头
  15. 服务器怎么用ftp传文件夹吗,ftp服务器怎么传文件夹吗
  16. 一篇文章带您秒懂地理标志商标注册
  17. 读书笔记——好句摘抄
  18. HZNUOJ 2977 宝可梦决战 种族并查集
  19. 1036:镂空三角形
  20. 一次硬盘问题引起的服务器宕机

热门文章

  1. Nacos服务发现与调用
  2. tracert路由跟踪命令分析判断
  3. CSS Modules和Styled Components
  4. linux usb root_hub中断传输
  5. YOLOv8模型网络结构图
  6. 数据库笔记(4)SQL练习题及Python与数据库交互
  7. Python说:这个炒股指标是我见过最废的,没有之一
  8. linux 下如何获取 cpu 温度
  9. 十进制数与N进制数据的转换
  10. UOS双系统启动菜单顺序修改方法