JAVA线程安全

今天把线程安全的又修改了一下,补充了一些,看起来也更明白。

线程安全

      什么是线程安全?

线程安全就是多线程访问同一段代码,不会产生不确定的结果。线程不安全则与之相反。

     为什么又会产生不确定的结果呢?

      首先要明白线程的工作原理:

JVM有主内存,主内存中的数据是多个线程共享的。而每个线程又有自己的工作内存。当线程对变量执行某个操作的时候:

1.先将该变量从主存中复制到工作内存(read ,load),

2.然后在工作内存中对该变量进行操作(use ,assign)

3.最后用工作内存数据去刷新主存(store,write)。

       所以如果多个线程同时操作这个变量的时候,就可能会出现预想不到想情况,简单来说就是一个线程在处理共享数据的时候,还没处理完毕就有另外的线程加入进来,导致共享数据存在了安全问题。

        例如现有个变量sum=0,有两个线程,线程A执行sum++;   线程B执行sum--;

线程A先运行,如果执行到第二步的时候,A的工作内存中的值为1,此时CPU调度让A暂停,B得到运行的机会,执行到第二步的时候,B的工作内存中的值为-1;再之后,若A先执行第3步,B后执行,则sum值为-1.相反,sum值为1;这就产生了上面说的不确定的结果,即线程不安全。


示例:

  public class Account {  
	  
    private int balance;  
  
    public Account(int balance) {  
        this.balance = balance;  
    }  
  
    public int getBalance() {  
        return balance;  
    }  
  
    public   void add(int num) {  
        balance = balance + num;  
    }  
  
    public   void withdraw(int num) {  
        balance = balance - num;  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        Account account = new Account(1000);  
        Thread a = new Thread(new AddThread(account, 20));  
        Thread b = new Thread(new WithdrawThread(account, 20));  
        a.start();  
        b.start();  
        a.join();  
        b.join();  
        System.out.println(account.getBalance());  
    }  
  
    static class AddThread implements Runnable {  
        Account account;  
        int     amount;  
  
        public AddThread(Account account, int amount) {  
            this.account = account;  
            this.amount = amount;  
        }  
  
        public void run() {  
				
		
            for (int i = 0; i < 10000; i++) {  
                account.add(amount);  
            }
            System.out.println("add="+account.getBalance());
        }  
    }  
  
    static class WithdrawThread implements Runnable {  
        Account account;  
        int     amount;  
  
        public WithdrawThread(Account account, int amount) {  
            this.account = account;  
            this.amount = amount;  
        }  
  
        public void run() {  
				
			
            for (int i = 0; i < 10000; i++) {  
                account.withdraw(amount);  
            } 
            
            System.out.println("withdraw="+account.getBalance());
        }  
    }  
}

多次执行得到的结果都不一样,无法确定结果,这是因为线程的执行顺序是不可预见的。这是JAVA同步机制产生的根源。有两种方法:

方法1:同步代码块;

方法2:同步方法;

       synchronized作为一种手段,解决了多线程执行的有序性和内存可见性,volatile解决了内存可见性。


Synchronized关键字:

JAVA用synchronize的关键字保证了多线程执行的有序性。当一段代码会修改线程的共享变量,那么这段代码变成了互斥区或者临界区,方法1(同步代码块):

synchronized(锁){
     临界区代码(需要被同步的代码)
}

临界区代码就是操作共享数据的代码。锁也叫做同步监视器,可由任何一个类的对象来充当,一般用Object类对象或者this (在实现的方式中,可以考虑用this,如果线程是继承的方式,要慎用,因为可能多个线程有多个this,可以考虑用static变量。) .

代码如下:

<span style="font-size:14px;">public class Account {  
	  
    private int balance;  
    static Object obj=new Object();
  
    public Account(int balance) {  
        this.balance = balance;  
    }  
  
    public int getBalance() {  
        return balance;  
    }  
    
    public   void add(int num) {  
        balance = balance + num;  
    }  
    
    public   void withdraw(int num) {  
        balance = balance - num;  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        Account account = new Account(1000);  
        Thread a = new Thread(new AddThread(account, 20));  
        Thread b = new Thread(new WithdrawThread(account, 20));  
        a.start();  
        b.start();  
        a.join();  
        b.join();  
        System.out.println(account.getBalance());  
    }  
  
    static class AddThread implements Runnable {  
        Account account;  
        int     amount;  
  
        public AddThread(Account account, int amount) {  
            this.account = account;  
            this.amount = amount;  
        }  
  
        public void run() {  
				
		
            synchronized (Account.obj) {//修改的地方
				for (int i = 0; i < 10000; i++) {
					account.add(amount);
				}
				System.out.println("add=" + account.getBalance());
			}
        }  
    }  
  
    static class WithdrawThread implements Runnable {  
        Account account;  
        int     amount;  
  
        public WithdrawThread(Account account, int amount) {  
            this.account = account;  
            this.amount = amount;  
        }  
  
        public void run() {  
				
			
            synchronized (Account.obj) {//修改的地方
				for (int i = 0; i < 10000; i++) {
					account.withdraw(amount);
				}
				System.out.println("withdraw=" + account.getBalance());
			}
        }  
    }  
}</span>

特别要注意锁的唯一性,即多个线程的锁应该是同一个对象。哪个线程获取了这个对象,就可以执行临界区的代码,其他线程就要等待。要求所有线程共用一个锁。

同步原理分析:

obj锁有2种状态,如果把它想象成红绿灯,在没有线程进入临界区之前是绿灯,容许线程进入。一旦一个线程进入了临界区,它就变成了红灯,不允许后面的线程进入。即使该线程在临界区里面sleep,后面的线程也不能进入。只有当该线程执行完该方法,离开了临界区,灯又变成了绿灯,才允许后面的线程进入。是不是很像火车上的厕所尴尬


方法2(同步方法):

将操作共享数据的方法用synchronized声明。

        当用synchronized关键字声明一个方法,那么对象的锁将保护整个方法,要调用该方法,就要获得该对象的锁。 每个锁对象都有两个队列,一个存储就绪线程,一个存储阻塞线程。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。当一个线程被唤醒(notify)时,就会进入就绪队列,等待CPU调度。例如:若线程a第一次执行account.add方法,此时,JVM检查就绪队列是否有线程在等待,若有,则说明account的锁已被占用。因为是第一次执行,就绪队列为空,线程a获得account的锁,然后执行account.add方法。如果在此时,线程b要执行account.withdraw方法,因为锁在线程a中,所以b无法执行,b进入account的就绪队列,等待a释放锁后才能执行。


在用synchronized声明方法后:

同步方法:

 public  synchronized void add(int num) {  
        balance = balance + num;  
    }  
  
    public  synchronized void withdraw(int num) {  
        balance = balance - num;  
    }  
得到的结果是唯一的。

一个线程执行临界区代码的步骤如下:

1.获得同步锁

2.清空工作内存

3.将主存的变量拷贝到工作内存

4.对变量进行操作

5.将操作后的变量写回主存

6.释放锁

此步骤也符合线程的工作原理。


Volatile关键字:

Volatile是轻量级的同步,因为它只能保证线程的可见性,不能保证有序性。而要彻底得保证线程执行的有序性和可见性,例如synchronized。用volatile声明的变量,它的修改是及时写在主存中的,而不是拷贝过去的副本,因此它能保证线程的可见性,修改变量能立即被其他线程所看见。但它不能保证线程执行的有序性,






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值