1001--Java 多线程 并发编程
发布日期:2021-06-28 19:49:21 浏览次数:2 分类:技术文章

本文共 3648 字,大约阅读时间需要 12 分钟。

1、synchronized关键字

一、多线程

1、操作系统有两个容易混淆的概念,进程和线程

进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据

不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。

线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。

同一进程中的线程共用相同的地址空间,同时共享进程锁拥有的内存和其他资源。

2、Java标准库提供了进程和线程相关的API。

进程主要包括表示进程的java.lang.Process类和创建进程的java.lang.ProcessBuilder类;

表示线程的是java.lang.Thread类,在虚拟机启动之后,通常只有Java类的main方法这个普通线程运行,

,运行时可以创建和启动新的线程;还有一类守护线程(damon thread),守护线程在后台运行,

提供程序运行时所需的服务。当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。

3、线程间的可见性:一个线程对进程中共享的数据的修改,是否对另一个线程可见

可见性问题:

a、CPU采用时间片轮转不同算法来对线程进行调度

public class IdGenerator{   private int value = 0;   public int getNext(){      return value++;   }}

对于IdGenerator的getNext()方法,在多线程下不能保证返回值是不重复的:

各个线程之间相互竞争CPU时间来获取运行机会,CPU切换可能发生在执行间隙。

以上代码getNext()的指令序列:CPU切换可能发生在7条指令之间,多个getNext的指令交织在一起。

aload_0dupgetfield #12dup_x1iconst_1iaddputfield #12

b、CPU缓存:

目前CPU一般采用层次结构的多级缓存的架构,有的CPU提供了L1、L2和L3三级缓存。

当CPU需要读取主存中某个位置的数据时,会一次检查各级缓存中是否存在对应的数据。

如果有,直接从缓存中读取,这比从主存中读取速度快很多。当CPU需要写入时,数据先被写入缓存中,

之后再某个时间点写回主存。所以某些时间点上,缓存中的数据与主存中的数据可能是不一致。

c、指令顺序重排:

出行性能考虑,编译器在编译时可能会对字节代码的指令顺序进行重新排列,以优化指令的执行顺序,

在单线程中不会有问题,但在多线程可能产生与可见性相关的问题。

---------------------------------------------------------------------------------------------

二、Java内存模型(Java Memory Model)

屏蔽了CPU缓存等细节,只关注主存中的共享变量;关注对象的实例域、静态域和数组元素;关注线程间的动作。

1、volatile关键词:用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。

(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。用来确保对一个变量的修改被正确地传播到其他线程中。

例子:A线程是Worker,一直跑循环,B线程调用setDone(true),A线程即停止任务

public class Worker{   private volatile boolean done;   public void setDone(boolean done){      this.done = done;   }   public void work(){      while(!done){         //执行任务;      }   }}

例子:错误使用。因为没有锁的支持,volatile的修改不能依赖于当前值,

当前值可能在其他线程中被修改。(Worker是直接赋新值与当前值无关)

public class Counter {    public volatile static int count = 0;    public static void inc() {        //这里延迟1毫秒,使得结果明显        try {            Thread.sleep(1);        } catch (InterruptedException e) {        }        count++;    }    public static void main(String[] args) {        //同时启动1000个线程,去进行i++计算,看看实际结果        for (int i = 0; i < 1000; i++) {            new Thread(new Runnable() {                @Override                public void run() {                    Counter.inc();                }            }).start();        }        //这里每次运行的值都有可能不同,可能不为1000        System.out.println("运行结果:Counter.count=" + Counter.count);    }}

2、final关键词

final关键词声明的域的值只能被初始化一次,一般在构造方法中初始化。。(在多线程开发中,final域通常用来实现不可变对象)

当对象中的共享变量的值不可能发生变化时,在多线程中也就不需要同步机制来进行处理,故在多线程开发中应尽可能使用不可变对象

另外,在代码执行时,final域的值可以被保存在寄存器中,而不用从主存中频繁重新读取。

3、java基本类型的原子操作

1)基本类型,引用类型的复制引用是原子操作;(即一条指令完成)

2)long与double的赋值,引用是可以分割的,非原子操作;

3)要在线程间共享long或double的字段时,必须在synchronized中操作,或是声明成volatile

三、Java提供的线程同步方式

1、synchronized关键字

方法或代码块的互斥性来完成实际上的一个原子操作。

(方法或代码块在被一个线程调用时,其他线程处于等待状态)

所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。

a、静态方法:Java类对应的Class类的对象所关联的监视器对象。

b、实例方法:当前对象实例所关联的监视器对象。

c、代码块:代码块声明中的对象所关联的监视器对象。

注:当锁被释放,对共享变量的修改会写入主存;当获得锁,CPU缓存中的内容被置为无效。

编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,

从而避免了由于代码重排而造成的问题。

例:以下方法getNext()和getNextV2() 都获得了当前实例所关联的监视器对象

public class SynchronizedIdGenerator{   private int value = 0;   public synchronized int getNext(){      return value++;   }   public int getNextV2(){      synchronized(this){         return value++;      }   }}

2、Object类的wait、notify和notifyAll方法

生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。

如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。

wait: 将当前线程放入该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,

并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,

否则直接抛出java.lang.IllegalMonitorStateException异常)。




转载地址:https://blog.csdn.net/xxxcyzyy/article/details/48321457 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:1002--并发
下一篇:1012--反射!!!

发表评论

最新留言

感谢大佬
[***.8.128.20]2024年04月27日 03时23分50秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章