目录

线程同步synchronized

Lock锁


线程同步synchronized

我们先看一个例子,假设去银行取钱,原始钱为20,每次只取1元,有3个人分别同时去取

package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private int a = 20;
    private boolean b = true;

    @Override
    public void run() {
        while (b) {
            if (a > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "a的结果" + a);
                a--;
            } else {
                b = false;
            }
        }

    }
}
package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:42
 */
public class SynchronizedDemo {
    public static void main(String[] args) {
        Test1 te = new Test1();
        Thread t1 = new Thread(te,"线程1");
        Thread t2 = new Thread(te,"线程2");
        Thread t3 = new Thread(te,"线程3");

        t1.start();
        t2.start();
        t3.start();
    }
}

我们多开了3个线程,共享数据a,每执行以下,a--

输出结果:

线程2a的结果20
线程3a的结果19
线程1a的结果19
线程2a的结果17
线程1a的结果16
线程3a的结果16
线程2a的结果14
线程3a的结果13
线程1a的结果13
线程2a的结果11
线程1a的结果10
线程3a的结果9
线程2a的结果8
线程3a的结果7
线程1a的结果7
线程2a的结果5
线程1a的结果4
线程3a的结果4
线程2a的结果2
线程3a的结果1
线程1a的结果0
线程2a的结果-1

从结果中,我们看到不仅有相同的a数值输出,还出现了0和-1,这就引发除了线程数据安全问题。

为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

如何解决多线程安全问题呢?

  • 基本思想:让程序没有安全问题的环境

怎么实现呢?

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
  • Java提供了同步代码块的方式解决
  • 格式

             synchronazed(任意对象){

                     多条语句操作共享数据的代码

             }

package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private int a = 20;
    private boolean b = true;

    @Override
    public void run() {
        while (b) {
            synchronized (new Object()) {
                if (a > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "a的结果" + a);
                    a--;
                } else {
                    b = false;
                }
            }
        }

    }
}

结果:

线程1a的结果20
线程3a的结果20
线程2a的结果18
线程3a的结果17
线程1a的结果17
线程2a的结果15
线程1a的结果14
线程3a的结果14
线程2a的结果12
线程1a的结果11
线程3a的结果11
线程2a的结果9
线程1a的结果8
线程3a的结果8
线程2a的结果6
线程3a的结果5
线程1a的结果5
线程2a的结果3
线程3a的结果2
线程1a的结果2
线程2a的结果0
线程3a的结果-1

咦,问题没解决啊,那是肯定的,因为你每次锁的时候,锁的对象都是new Object(),相当于没锁,所以我们要把代码改进一下

package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private int a = 20;
    private boolean b = true;
    private Object obj = new Object();

    @Override
    public void run() {
        while (b) {
            synchronized (obj) {
                if (a > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "a的结果" + a);
                    a--;
                } else {
                    b = false;
                }
            }
        }

    }
}

结果

线程1a的结果20
线程1a的结果19
线程1a的结果18
线程1a的结果17
线程1a的结果16
线程1a的结果15
线程1a的结果14
线程1a的结果13
线程1a的结果12
线程1a的结果11
线程1a的结果10
线程1a的结果9
线程1a的结果8
线程1a的结果7
线程3a的结果6
线程3a的结果5
线程3a的结果4
线程3a的结果3
线程3a的结果2
线程3a的结果

这边解决了线程安全问题

如果我们不想要这个private Object obj = new Object();怎么办,并且想把方法拿出来怎么办,那我们就改进一下,Java提供了同步的方法,以下2种的代码是一样的,是同一个锁

package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private int a = 20;
    private boolean b = true;

    @Override
    public void run() {
        while (b) {
            synchronized (this) {
                if (a > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "a的结果" + a);
                    a--;
                } else {
                    b = false;
                }
            }
        }

    }
}
package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private int a = 20;
    private boolean b = true;

    @Override
    public void run() {
        while (b) {
            mathod();
        }

    }

    //这里锁的是this
    private synchronized void mathod() {
        if (a > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "a的结果" + a);
            a--;
        } else {
            b = false;
        }
    }
}

 如果mathod方法是静态的呢,那么我们锁的就是类的字节码文件,以下2种锁的是同一个锁

package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private static int a = 20;
    private static boolean b = true;

    @Override
    public void run() {
        while (b) {
            mathod();
        }

    }

    //这里锁的是Test1.class
    private static synchronized void mathod() {
        if (a > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "a的结果" + a);
            a--;
        } else {
            b = false;
        }
    }
}
package com.Thread;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private static int a = 20;
    private static boolean b = true;

    @Override
    public void run() {
        while (b) {
            synchronized (Test1.class) {
                if (a > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "a的结果" + a);
                    a--;
                } else {
                    b = false;
                }
            }
        }

    }

}

了解了线程安全之后,我们来看看几个线程安全的类

  • StringBuffer:用法等同于StringBuilder,只不过是StringBuilder是单线程的,而StringBuffer是线程安全的
  • Vector:用法等同于ArrayList,只不过ArrayList是单线程的,而Vector是线程安全的
  • Hashtable:用法等同于HashMap,只不过HashMap是单线程的,而Hashtable是线程安全的

因为线程安全涉及到锁。所以效率会相应的降低,所以一般我们用StringBuffer,而很少用VectorHashtable

集合的线程安全用法由集合的工具类Collections提供

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}

例:

List<Object> objects = Collections.synchronizedList(new ArrayList<>());
Map<Object, Object> objectObjectMap = Collections.synchronizedMap(new HashMap<>());
Lock锁

虽然我们理解了synchronized,但是我们看不到具体在哪里加了锁和解锁,JDK5之后,Java提供了新的锁对象Lock

Lock实现提供比使用synchronized方法和语句可以获得更广发的锁操作。它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个关联的Condition对象。

Lock中提供了获得锁和释放锁的方法:

  • void lock():获得锁
  • void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现了类ReentrantLock来实例化

ReentrantLock的构造方法

  • ReentrantLock():创建一个ReentrantLock的实例
package com.Thread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private int a = 20;
    private boolean b = true;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (b) {
            // 加锁
            lock.lock();
            if (a > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "a的结果" + a);
                a--;
            } else {
                b = false;
            }
            // 释放锁
            lock.unlock();
        }

    }
}

上面的代码也解决了数据安全的问题,只要把出现涉及数据安全的代码放在加锁与释放锁之间即可,但是这样还要缺陷,加入加锁与释放锁之间的代码出现异常,那么我们的释放锁就没有执行,也就没有释放锁,所以代码进一步改进

package com.Thread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 林高禄
 * @create 2020-05-14-15:43
 */
public class Test1 implements Runnable {
    private int a = 20;
    private boolean b = true;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (b) {
            // 加锁
            try {
                lock.lock();
                if (a > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "a的结果" + a);
                    a--;
                } else {
                    b = false;
                }
            } finally {
                // 释放锁
                lock.unlock();
            }

        }

    }
}

这样不管如何,锁都会释放