java 多线程 --同步

多线程的同步

为什么引入同步机制

多线程为什么要采用同步机制,因为不同的线程有自己的栈,栈中可能引用了多个对象,而多个线程可能引用到了堆中的同一个或多个对象,而线程的栈内存当中的数据只是临时数据,最终都是要刷新到堆中的对象内存,这里的刷新并不是最终的状态一次性刷新,而是在程序执行的过程中随时刷新(肯定有固定的机制,暂不考虑),也许在一个线程中被应用对象中的某一个方法执行到一半的时候就将该对象的变量状态刷新到了堆的对象内存中,那么再从多线程角度来看,当多个线程对同一个对象中的同一个变量进行读写的时候,就会出现类似数据库中的并发问题。

假设银行里某一用户账户有1000元,线程A读取到1000,并想取出这1000元,并且在栈中修改成了0但还没有刷新到堆中,线程B也读取到1000,此时账户刷新到银行系统中,则账户的钱变成了0,这个时候也想去除1000,再次刷新到行系统中,账号的钱变成0,这个时候A,B都取出1000元,但是账户只有1000,显然出现了问题。针对上述问题,假设我们添加了同步机制,那么就可以很容易的解决。

怎样解决这种问题呢,在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。

代码:

package com.java.test;

/**
 * Created by xiaofandiy03 on 2018/4/14.
 */
public class     DrawMoneyTest {
    public static void main(String[] args)
    {
        Bank bank = new Bank();

        Thread t1 = new MoneyThread(bank);// 从银行取钱
        Thread t2 = new MoneyThread(bank);// 从取款机取钱

        t1.start();
        t2.start();

    }
}
class Bank{
    private int money =1000;
    public int getMoney(int number)
    {
        if(number <0)
        {
            return -1;
        }else if(number >money) {
            return  -2;
        }else if(money <0)
        {
            return -3;
        }else {
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
            money-=number;
        System.out.println("Left Money :" +money);
        return  number;
    }
}
class MoneyThread extends Thread
{
    private Bank bank;

    public MoneyThread(Bank bank)
    {
        this.bank = bank;
    }

    @Override
    public void run()
    {
        System.out.println(bank.getMoney(1000));
    }
}复制代码

怎么解决这种问题呢,解决的方案是加锁

你想要进行对一组加锁的代码进行操作吗?想的话就先拿到锁,拿到锁之后就可以操作被加锁的代码,倘若拿不到锁的话就只能等着,因为等的线程太多了,这就是线程的阻塞。

竞态条件和内存可见性

线程和线程之间是共享内存的,当多线程对共享内存进行操作的时候有几个问题是难以避免的,竞态条件和内存可见性。

竞态条件

当多线程访问和操作同一对象的时候计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件

最常见的竞态条件为:

  1. 先检测后执行。执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。
  2. 延迟初始化(最典型即为单例)

上文中说到的加锁就是为了解决这个问题,常见的解决方案有:

  • 使用synchronized关键字
  • 使用显式锁(Lock)
  • 使用原子变量

内存可见性

关于内存可见性问题要先从内存和cpu的配合谈起,内存是一个硬件,执行速度比CPU慢几百倍,所以在计算机中,CPU在执行运算的时候,不会每次运算都和内存进行数据交互,而是先把一些数据写入CPU中的缓存区(寄存器和各级缓存),在结束之后写入内存。这个过程是及其快的,单线程下并没有任何问题。

但是在多线程下就出现了问题,一个线程对内存中的一个数据做出了修改,但是并没有及时写入内存(暂时存放在缓存中);这时候另一个线程对同样的数据进行修改的时候拿到的就是内存中还没有被修改的数据,也就是说一个线程对一个共享变量的修改,另一个线程不能马上看到,甚至永远看不到。

这就是内存的可见性问题。

解决这个问题的常见方法是:

  • 使用volatile关键字
  • 使用synchronized关键字或显式锁同步

线程同步方法

同步方法:

即有synchronized关键字修饰方法。悠悠java每个对象都有一个内置锁,放用关键字修饰方法时,内置所会保护整个方法。在调用该方法钱,获得内置锁,否则就处于阻塞状态。

class Bank{
    private int money =1000;
    public synchronized int getMoney(int number)
    {
        if(number <0)
        {
            return -1;
        }else if(number >money) {
            return  -2;
        }else if(money <0)
        {
            return -3;
        }else {
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
            money-=number;
        System.out.println("Left Money :" +money);
        return  number;
    }
}复制代码

 synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

同步代码块

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

class Bank{
    private int money =1000;
    public  int getMoney(int number)
    {
        synchronized (this) {
            if (number < 0) {
                return -1;
            } else if (number > money) {
                return -2;
            } else if (money < 0) {
                return -3;
            } else {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            money -= number;
        }
        System.out.println("Left Money :" +money);
        return  number;
    }
}
复制代码

同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

使用重入锁实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 

eentrantLock具有和synchronized相似的作用,但是更加的灵活和强大。

它是一个重入锁(synchronized也是),所谓重入就是可以重复进入同一个函数,这有什么用呢?

假设一种场景,一个递归函数,如果一个函数的锁只允许进入一次,那么线程在需要递归调用函数的时候,应该怎么办?退无可退,有不能重复进入加锁的函数,也就形成了一种新的死锁。

重入锁的出现就解决了这个问题,实现重入的方法也很简单,就是给锁添加一个计数器,一个线程拿到锁之后,每次拿锁都会计数器加1,每次释放减1,如果等于0那么就是真正的释放了锁。

volatile 关键字

当一个共享变量被volatile修饰的时候,他会保证变量被修改之后立马在内存中更新,另一线程在取值时候需要去内存中读取新的值。

volatile可以保证变量的内存可见性,但是不能保证原子性,对于b++这个操作来说,并不是一步到位的,而是分好几步的,读取白那两,定义常量1,变量b加1,结果同步到内存。虽然在每一步中获取的都是变量的最新值,但是没有保证b++的原子性,自然无法做到线程安全。

使用局部变量实现线程同步

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,知识名字相同而已。所以就会发生上面的效果。

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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 您好!如果我理解正确的话,您的问题是关于JMU Java 07多线程同步访问的。以下是我的回答: 多线程编程是指在一个程序同时执行多个线程的编程技术。多线程编程在提高程序性能、增强程序交互性等方面有很大的优势,但也会引入一些问题,比如线程安全问题。 当多个线程同时访问共享资源时,可能会出现线程不安全的问题,比如数据竞争、死锁等。为了避免这些问题,我们需要使用同步机制来确保线程安全。 Java提供了多种同步机制,比如synchronized关键字、ReentrantLock类、Semaphore类等。这些同步机制都可以用来保证多线程访问共享资源的安全性。 其,synchronized是最常用的同步机制之一。在Java,synchronized关键字可以用来修饰方法或代码块,以实现同步访问共享资源。synchronized可以保证在同一时间只有一个线程可以访问共享资源,从而避免了数据竞争等线程安全问题。 除了synchronized之外,Java还提供了一些其他的同步机制,比如ReentrantLock类和Semaphore类。ReentrantLock类提供了比synchronized更灵活的锁机制,可以实现可重入锁、公平锁等功能。Semaphore类可以控制同时访问共享资源的线程数量,从而避免资源被过度占用的问题。 总的来说,多线程编程是一项复杂而重要的技术,需要仔细研究和实践。在实际开发,我们需要根据具体的需求选择合适的同步机制来确保多线程访问共享资源的安全性。 ### 回答2: 多线程编程是在当前计算机领域最为常见的技术之一,它可以利用计算机的多核处理器来使程序运行更加高效。但是,多线程编程可能会出现的最大问题就是线程安全,因为线程之间可能会访问相同的资源,从而导致竞态条件。 在Java,可以通过使用synchronized关键字来实现同步访问,从而避免线程安全问题。synchronized关键字可以用于两种不同的情形:同步方法和同步块。在同步方法,方法是同步的,即每个线程在执行该方法时都需要获取该对象的锁,如果该锁已经被其他线程获取,则需要等待直到此锁被释放。在同步,需要手动指定锁,即每个线程在执行同步块时需要获取该指定锁,其他线程如果需要访问该代码块的共享资源也需要获取该指定锁,这样就保证了该代码块的所有共享资源的同步访问。 除了synchronized关键字外,Java还提供了其他一些同步机制来实现线程安全,如ReentrantLock类和CountDownLatch类等。ReentrantLock类可以实现更为灵活的同步访问控制,但需要手动释放锁;而CountDownLatch类则用于同步一个或多个线程,使这些线程在某个条件满足之前一直处于等待状态。 在进行多线程编程时,应该尽量避免对同步访问造成瓶颈,应该通过减小同步代码块的范围等方式来提高程序的效率。此外,多线程编程时还应该进行线程安全性的测试,以确保程序能够正确地运行。 ### 回答3: 在Java多线程是一种非常常见的编程方式。由于多线程的特点,对共享资源的访问会出现竞争的情况,这种竞争可能会导致数据不一致或程序异常等问题。因此,在多线程编程,我们需要采取一些措施来保证共享资源的访问能够正确、有序地进行,这就是同步机制同步机制包括两种方式:锁和信号量。锁是最基本的同步机制。锁有两种类型:互斥锁(Mutex)和读写锁(ReadWriteLock)。互斥锁用于保护共享资源,保证同一时间只有一个线程可以访问它,其他线程需要等待锁释放后才能继续访问。读写锁用于读写分离场景,提高了多线程访问共享资源的并发性。读写锁支持多个线程同时读取共享资源,但只允许一个线程写入共享资源。 信号量是一种更加高级的同步机制。信号量可以用来控制并发线程数和限制访问共享资源的最大数量。在Java,Semaphore类提供了信号量的实现。Semaphore可以控制的线程数量可以是任意的,线程可以一起执行,也可以分批执行。 除了锁和信号量,Java还提供了一些其他同步机制,比如阻塞队列、Condition等。阻塞队列是一种特殊的队列,它支持线程在插入或者删除元素时阻塞等待。Condition是一种锁的增强,它可以让线程在某个特定条件下等待或者唤醒。 在多线程编程,使用同步机制需要注意以下几点。首先,同步机制要尽可能的保证资源访问的公平性,避免因为某些线程执行时间过长导致其他线程等待时间过长。其次,同步机制要尽可能的避免死锁的发生,尤其要注意线程之间的依赖关系。最后,同步机制的实现要尽可能地简单,避免过于复杂的代码实现带来的维护成本。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值