Java多线程之线程安全(1)总概况和synchronized

一.概括

本文主要内容

1.多线程使用过程中遇到的问题
2.造成问题的原因(想懂得原因Java内存模型是避不开的所以参考我的上一篇文章Java多线程之线程安全(0)Java内存区域与Java内存模型)
3.保证线程安全的方法(由于方法比较多,本文只介绍synchronized)
4.synchronized关键字

二.线程的基本使用

使用场景:车站有100张票,分给俩个窗口卖出去。
看代码如下所示Activity(因为是做Android的这么感觉更方便,所以在Activity中写了哦)

	public class ThreadTestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_test);

        use();
    }

    private void use() {
        Ticket ticket = new Ticket(100);

        Thread thread1 = new Thread(ticket,"窗口A");
        Thread thread2 = new Thread(ticket,"窗口B");

        thread1.start();
        thread2.start();
    }
}

Ticket类

public class Ticket implements Runnable{
    int count;
    int num = 1;

    public Ticket(int count){
        this.count = count;
    }

    @Override
    public void run() {
        while (true){
            sale();
        }
    }

    private void sale() {
        if (num <=count) {
            Log.e("SaleTicket", Thread.currentThread().getName() + "卖了第" + num + "张票");
            num++;
        } else {
            return;
        }
    }
}

下面是log的部分截图
Java多线程之线程安全(1)总概况和synchronized_第1张图片
从图中我们发现窗口A和B都卖了出了第27张票,这显然是不符合实际情况的。实际售票中肯定是一张票对应一个座位,如果一个座位售出去两次票,上车之后肯定打起来了。这就是线程中最简单的关于线程安全的例子。

三.线程安全问题的原因

上面一中出现的现象就是线程中最简单的关于线程安全的例子。(至于线程安全问题出现的根本原因请查看Java内存模型一文中最后3.1关于线程安全产生的原因,其实上一篇通篇都是在讲线程是如何工作的,并且最后引出了线程安全问题的根本原因)。这里就不做赘述,而本文的重点是如何解决线程安全问题。

线程安全问题的本质就是:不能同时保证原子性,可见性,有序性

四.解决线程安全问题

还是以上面一中的售票例子为主,上面的例子中关键是票是共享数据,分给两个线程来操作,只有当共享数据确保在每个线程中同步,也就是时刻一致才能避免上面问题的出现。有下面几种常用的方式可以保证线程安全。

4.1要注意的几个点

4.1.1volatile关键词:

1.保证了可见性
2.保证了有序性
但是不能保证原子性。一定程度上能保证线程安全。
原理:volatile用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。

4.1.2final关键词

可以修饰的对象
  1. 修饰类
  2. 修饰方法
  3. 修饰变量
使用的原因:

修饰类:当用final修饰一个类时,表明这个类不能被继承。
修饰方法:如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。
修饰变量:基本数据类型的变量赋值之后便不能改变;引用数据类型在指向一个实例之后便不能指向其他的实例了。
这里由于不是专门写final的,所以只是大概介绍一下,详细的介绍可以参考浅析Java中的final关键字

4.1.3java基本类型的原子操作

1.基本类型,引用类型的复制引用是原子操作;(即一条指令完成)但是有特殊的如下面俩
2.long与double的赋值,引用是可以分割的,非原子操作;
3.要在线程间共享long或double的字段时,必须在synchronized中操作,或是声明成volatile

4.2保障线程安全的几种方法

1.synchronized关键字(互斥锁)
2.Lock(java.util.concurrent.locks包下常用的类)
3.Atomic(java.util.concurrent.atomic包下常用的类)

五.synchronized关键字

5.1介绍

Java为了防止资源冲突提供了内置支持:关键字synchronized。所有对象都自动含有单一的锁(也称为监视器)。当任务要执行被synchronized修饰的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。

synchronized的作用主要有三个:

  1. 确保线程互斥的访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决重排序问题。

从语法上讲,Synchronized总共有三种用法:

  1. 修饰普通方法(锁对象)
  2. 修饰代码块(锁括号中的东西,可以是对象,也可以是类)
  3. 修饰静态方法(锁类)

下面就一一介绍各种使用方法

5.2修饰普通方法

上面“二”中的方法sale前面加synchronized关键字,其他的不变
如下

	private synchronized void sale() {
           if (num <= count){
               Log.e("SaleTicket",Thread.currentThread().getName() +"卖了第"+num+"张票");
               num++;
           }else {
               return;
           }
       }

下面是log部分截图发现已经没有了上面的错误。
Java多线程之线程安全(1)总概况和synchronized_第2张图片

保证一个对象实例中的变量在各个线程是同步的

5.3修饰代码块

将上面sale方法中的代码放在同步代码块中,其他不变,如下面代码。因为log一样这里就不重复粘贴了。

	private void sale() {
          synchronized (new Object()){
              if (num <= count){
                  Log.e("SaleTicket",Thread.currentThread().getName() +"卖了第"+num+"张票");
                  num++;
              }else {
                  return;
              }
          }

      }

这里借用了new Object()产生的对象的锁,保证同步代码块中num变量在各个线程中是同步的

5.4修饰静态方法

我们将“二”中的代码修改一下
Activity中

public class ThreadTestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread_test);

        use();
    }

    private void use() {
	    //相对于上面“二”中代码,多创建了一个Ticket实例。
        Ticket ticket1 = new Ticket();
        Ticket ticket2 = new Ticket();

        Thread thread1 = new Thread(ticket1,"窗口A");
        Thread thread2 = new Thread(ticket2,"窗口B");

        thread1.start();
        thread2.start();
    }
}

Ticket中

public class Ticket implements Runnable{
	//常量:总票数
    public static final int count = 100;
    //卖到哪一张票了(改为使用static修饰)
    public static int num = 1;

    public Ticket(){
    }

    @Override
    public void run() {
        while (true){
            sale();
        }
    }
	
	//改为static修饰
    private static void sale() {
        if (num <=count) {
            Log.e("SaleTicket", Thread.currentThread().getName() + "卖了第" + num + "张票");
            num++;
        } else {
            return;
        }
    }
}

这里只是截取部分log,但是如果你自己运行一下的话会发现,即使我创建了两个Ticket对象,运行结果似乎还和我创建一个是类似的。这里只是因为没有加同步锁的原因造成重复票的出现。
Java多线程之线程安全(1)总概况和synchronized_第3张图片

下面我们给静态方法添加synchronized,只对sale方法添加,其他代码不变。

	private synchronized static void sale() {
        if (num <=count) {
            Log.e("SaleTicket", Thread.currentThread().getName() + "卖了第" + num + "张票");
            num++;
        } else {
            return;
        }
    }

log和上面的一样,这里不粘贴了。
解释一下为什么会在创建了两个Ticket实例的时候效果和创建一个一样:这里面的num由于声明为了static的所以num作为类的变量存在,即使我们创建再多的Ticket实例,也是相同的效果,因为它们公用一个num。

同时,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法)

若果上面的例子看着不好理解,那么我们换一种写法。如下

	private void sale() {
        synchronized (Ticket.class){
            if (num <=count) {
                Log.e("SaleTicket", Thread.currentThread().getName() + "卖了第" + num + "张票");
                num++;
            } else {
                return;
            }
        }
    }

效果和上面是完全一样的效果,而这种写法更好理解。

5.5使用synchronized修饰防止线程安全问题出现的原因分析(理解synchronized关键字的关键所在)

方法或代码块的互斥性来完成实际上的一个原子操作。方法或代码块在被一个线程调用时,其他线程处于等待状态。所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。

  1. 实例方法:当前对象实例所关联的监视器对象。
  2. 代码块:代码块声明中的对象所关联的监视器对象。
  3. 静态方法:Java类对应的Class类的对象(也就是类,如上面的Ticket)所关联的监视器对象。
    换句话说就是对“实例”,“代码块中实例”,“类”进行同步化。

:当锁被释放,对共享变量的修改会写入主存;当活得锁,CPU缓存中的内容被置为无效。编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。

5.6常用的单例写法

public class SingleTon {
    private volatile static SingleTon instance;
    private SingleTon(){}

    public static SingleTon getInstance(){
        if (instance == null){//第一个判空操作
            synchronized (SingleTon.class){
                if (instance == null){第二个判空操作
                    instance = new SingleTon();
                }
            }
        }

        return instance;
    }
}

一个常见的双重检验单例,可能平时在写的时候我们会丢掉volatile。在单线程中确实不会带来问题,即使没有synchronized来加锁,但是在多线程中如果不加volatile的话,只是加锁的话这种单例也是会失效的。下面分析原因:
如果线程1和线程2同时进入第一个判空操作,线程1此时获得锁,线程2等待。直到线程1执行完实例化操作,此时线程二获得锁,此时线程一工作内存中的instance实例副本写到了主内存中,但是线程二中工作内存中的instance实例副本不知道主内存中instance实例已经发生了变化,造成线程2中instance实例仍为空,于是还是进入第二个判空操作中,造成两个SingleTon实例的产生,造成单例失效。使用了volatile之后由于他的可见性特征避免上述情况的产生。

你可能感兴趣的:(java)