线程同步和锁

线程的同步问题

线程的同步是为了防止多个线程同时访问一个数据对象时,对数据造成的破坏。
例如:
Thread-a和Thread-b同时操作一个数据Foo对象

/**
 * @author FengTianHao
 * @version 1.0
 * @since 2018/11/18 16:52
 */
public class Fool {
    private int x=100;
    public int getX()
    {
        return x;
    }

    public void fix(int y)
    {
        x=x-y;
    }
}
/**
  @author FengTianHao
  @version 1.0
  @since 2018/11/15 17:04
 */
public class TestThread implements Runnable {
    private Fool  foo=new Fool();
    @Override
    public void run()
    {
        for(int i=0;i<3;i++)
        {
            foo.fix(30);
            System.out.println(Thread.currentThread().getName()+foo.getX());
            try {
                Thread.sleep(30);//每到线程执行到这时会休眠30毫秒,给其他线程提供执行的机会
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args)
    {
        TestThread thread=new TestThread();
        Thread a=new Thread(thread,"Thread-a");
        Thread b=new Thread(thread,"Thread-b");
        a.start();
        b.start();
    }
}

运行结果:
在这里插入图片描述
这样的结果显然是不合理的,同时访问一个数据,结果应该是相同的,但上面运行结果却有不相同的时候,这就是对两个线程不加以控制访问Fool对象造成的。
要想结果具有合理性,我们应该对访问数据的线程做一些规定,例如每次只有一个线程能访问,这样就能保证数据的合理性。

在具体的java代码中通常采取以下两个操作:

1.将多个线程要访问的数据对象设为private型
2.同步哪些修改变量的代码时,用关键字synchronized同步方法或者代码块

同步和锁定

锁的原理

  • java中每个对象都有一个内置锁
  • 当程序运行到非静态的synchronized同步方法时,自动获得与正在中代码类的当前实例有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上同步、在对象上锁定
  • 当程序运行到synchronized同步的方法或者代码块时,对象锁才起作用
  • 一个对象只有一个锁,如果一个线程获得该锁,则没有其他线程可以获得该锁,除非获得该锁的线程将之释放。这意味着其他线程不能进入该对象的synchronized同步方法或者代码块。
  • 释放锁是指持有该锁的线程退出了synchronized同步方法或者代码块。

注意

  • 只能同步方法不能同步类和变量
  • 每个对象只有一个锁,提起同步的时候要清楚在哪个对象上同步
  • 不必同步所有的方法,类可以同时拥有同步方法和非同步方法
  • 如果两个线程同时访问一个类中的synchronized方法,则只有持锁线程才能访问该方法,其他的线程只有等到该锁被释放才能获得该锁并访问该方法。
  • 如果类有同步和非同步方法,非同步方法可以被多个线程任意访问而没有限制
  • 线程睡眠时,它持有的锁不会被释放
  • 线程可以获得多个锁,例如在一个同步方法里面调用另一个同步方法,此时该线程获得两把锁。

静态方法同步

要同步静态方法,需要一个用于整个类对象的锁,这个对象就是这个类(xxx.class)例如:

 public static synchronized int setName(String name)
    {
        Xxx.name=name;
    }
    等价于
    public static int setName(String name)
    {
        synchronized (Xxx.class)
        {
            Xxx.name=name;
        }
    }

如果线程不能获得锁会如何

如果线程企图进入同步方法,而其锁已经别占用,则线程会在该对象上被阻塞直到该对象上的锁被释放,该线程再次进入可执行状态。

当考虑阻塞时,一定要明白哪个对象被锁定:

  1. 调用同一个对象中的非静态方法的线程彼此阻塞,如果是不同对象则线程间彼此不干扰。
  2. 调用同一个类中的静态方法的线程彼此阻塞,它们锁定在相同的class对象。
  3. 静态同步方法和非静态同步方法永远不会阻塞,因为静态同步方法锁定在class对象上,非静态同步方法锁定在该类的对象上。
  4. 对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)在同一个对象上同步的线程彼此会阻塞,在不同对象上的同步的线程不会阻塞。

何时需要同步

在多个线程同时访问互斥数据时,应该同步来保护数据,确定两个线程不会同时对该数据进行修改。

线程安全类

  • 当一个类很好的用同步来保护它的数据时就称之为线程安全类
  • 即是是线程安全类也可能会出现问题,因为操作它的线程可能是不安全的
  • 举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。
    代码如下:
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * @author FengTianHao
 * @version 1.0
 * @since 2018/11/19 18:26
 */
public class Namelist {
    private List namelist= Collections.synchronizedList(new LinkedList());
    public void addName(String name)
    {
        namelist.add(name);
    }
    public String RemoveFirst()
    {
        if(namelist.size()>0)
        {
            return (String)namelist.remove(0);
        }
        else {
            return null;
        }
    }
}


/**
 * @author FengTianHao
 * @version 1.0
 * @since 2018/11/15 17:04
 */
public class TestThread  {

    public static void main(String[] args)
    {
        final Namelist namelist=new Namelist();
        namelist.addName("aaa");
        class ThreadTest extends Thread
        {
            @Override
            public void run()
            {
                String name=namelist.RemoveFirst();
                System.out.println(name);
            }
        }
        Thread t1=new ThreadTest();
        Thread t2=new ThreadTest();
        t1.start();
        t2.start();
    }
}

虽然集合对象是线程安全的,但是操作的程序确实不安全的,解决的方法是对操作的集合对象加个同步代码如下:

public synchronized String RemoveFirst()
    {
        if(namelist.size()>0)
        {
            return (String)namelist.remove(0);
        }
        else {
            return null;
        }
    }

这样的话当一个线程对这个对象的同步方法进行访问时,其他线程只能等待。

线程死锁

在java程序中,如果两个线程彼此都在等待对方因而进入阻塞状态,此时就会发生死锁。如下:

/**
 * @author FengTianHao
 * @version 1.0
 * @since 2018/11/19 18:46
 */
public class DeadLockRisk {
    private static class Resource
    {
        public int value;
    }
    private Resource ra=new Resource();
    private Resource rb=new Resource();
    public int read()
    {
        synchronized(ra)
        {
            synchronized (rb)
            {
                return ra.value+rb.value;
            }
        }
    }
    public void  write(int a,int b)
    {
        synchronized (rb)
        {
            synchronized (ra)
            {
                ra.value=a;
                rb.value=b;
            }
        }
    }
}

假如有同时有两个线程a b,a线程调用了read()方法的同时,b线程调用了write()方法,此时a线程拥有了锁ra,b线程有了锁rb,如果两个线程一直等下去的话就会发生死锁。

小结

  1. 线程同步的目的是为了保护多个线程访问一个资源对资源的破坏。
  2. 线程同步的实现是通过锁,一个对象只有一个锁,每个锁都特定对应一个对象;当一个线程获得该对象的锁时,其他线程就不能访问该对象的同步方法。
  3. 对于静态同步方法,锁定的是这个类的class对象,而非静态同步方法,锁定的是该类的对象。
  4. 对于同步要时刻清楚在哪个对象上同步。
  5. 多个线程等待一个对象的锁时,没有获得锁的线程将会阻塞。
  6. 死锁时线程之间相互等待锁造成的。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值