目录
一、线程安全概述
生活中我们往往会出现多个人共用同一个事物的情况。比如说两个人可以共用一个游戏账号,这样两个人都能使用这个账号来进行游戏。如果两个人能确保一个人在使用这个账号打游戏时另外一个人不会使用这个账号,那么自然没有问题。但是由于地理隔绝,当一个人登录进行游戏时,另外一个并不知道他正在打游戏,这时有可能会出现两个人同时登录的情况。那么这时候到底是让原来的人继续玩还是让后面的人登录呢?如果两个人是同时登录的话又应该算谁登录成功呢?由此可见,多个线程在操作共享资源时,如果不加以一定条件的限制就会出现一些矛盾的情况。这种情况就称为线程不安全。
如果我们将账号登录添加条件:当账号在一个设备登录时,别的设备不能进行登录,那么上面的问题也就解决了。当多个线程通过一定的限制来操作共享资源,使得一个线程在操作共享资源时,其他线程不能操作该共享资源,只能等待该线程操作完毕后才能操作该共享资源时,我们称这个过程是线程安全的。
例:创建两个线程,共同输出数字1到100.(这个例子是线程不安全的)
package test.thread;
//通过实现Runnable接口创建多线程
public class ThreadTest1 implements Runnable {
int i=1;
//重写run方法
public void run() {
//获取线程名字
String name=Thread.currentThread().getName();
//输出数字1到100
while(i<=100) {
System.out.println("线程"+name+"输出"+i);
i++;
}
}
}
package test.thread;
public class ThreadTest {
public static void main(String []args) {
ThreadTest1 thread=new ThreadTest1();
Thread thread1=new Thread(thread);
Thread thread2=new Thread(thread);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}输出结果不安全:输出了两次1
线程thread1输出1
线程thread1输出2
线程thread1输出3
线程thread1输出4
线程thread1输出5
线程thread1输出6
线程thread2输出1
二、线程同步
解决线程安全问题的方法叫做线程同步。在Java中,我们通过同步代码块、同步方法和lock锁的方式来实现线程同步进而解决线程安全问题。接下来我们将逐一介绍上面提到的三个方法。
同步代码块
同步代码块实现线程同步的格式如下:
synchronized(同步监视器){
//需要被同步的方法
}
·在这里,synchronized是Java中的关键字,就是用来表示同步的。
·同步监视器可以是任何类的一个对象。
·需要被同步的方法就是操作共享数据的代码。
多线程执行同步代码块的过程可以表示如下:多个线程共同竞争来获取同步监视器,当一个线程获得同步监视器后,别的线程边不能再获取该同步监视器直到获取了同步监视器的线程释放同步监视器其他线程才可以获取这个同步监视器,而获取同步监视器的线程则执行被同步的方法。
例:创建两个线程共同输出数字1到100,要求是线程安全的。
package test.thread;
//通过实现Runnable接口创建多线程
public class ThreadTest1 implements Runnable {
int i=1;
Double d=1.1;
//重写run方法
public void run() {
String name=Thread.currentThread().getName();
//同步代码块
synchronized(d) {
while(i<=100) {
System.out.println("线程"+name+"输出"+(i-1));
i++;
}
}
}
}
package test.thread;
public class ThreadTest {
public static void main(String []args) {
ThreadTest1 thread=new ThreadTest1();
Thread thread1=new Thread(thread);
Thread thread2=new Thread(thread);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}同步方法
如果共享资源是一个方法,我们可以给这个方法加上synchronized关键字,将其声明为同步方法(其实就是相当于同步代码块中的同步监视器用this代替时)
同步方法格式:
权限修饰符 synchronized 返回值类型 方法名(形参列表)
例:用同步方法的形式实现上面的例子
package test.thread;
//通过实现Runnable接口创建多线程
public class ThreadTest1 implements Runnable {
int i=1;
Double d=1.1;
//重写run方法
public void run() {
String name=Thread.currentThread().getName();
this.symp1(name);
}
//同步方法
public synchronized void symp1(String name) {
while(i<=100) {
System.out.println("线程"+name+"输出"+(i-1));
i++;
}
}
}
package test.thread;
public class ThreadTest {
public static void main(String []args) {
ThreadTest1 thread=new ThreadTest1();
Thread thread1=new Thread(thread);
Thread thread2=new Thread(thread);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}使用Lock锁
使用Lock锁的步骤如下:
①创建ReentrantLock实例化对象
②在共享代码前面调用上述实例化对象的lock()方法
③在共享代码后面调用上述实例化对象的unlock()方法
例:用Lock锁的方式完成上面例题
package test.thread;
import java.util.concurrent.locks.ReentrantLock;
//通过实现Runnable接口创建多线程
public class ThreadTest1 implements Runnable {
int i=1;
ReentrantLock lock=new ReentrantLock();
//重写run方法
public void run() {
String name=Thread.currentThread().getName();
lock.lock();
try {
while(i<=100) {
System.out.println("线程"+name+"输出"+(i));
i++;
}
}finally {
lock.unlock();
}
}
}
//主类同上三、注意事项
- wait()会释放锁,sleep()不会释放锁。
- 任何类的一个对象都可以作为同步监视器。
- 只有使用的是同一个对象作为同步监视器才能起到同步的效果。
- 如果两个线程彼此占有对方所需要的锁不释放就会进入死锁状态,都在等待对方释放锁,此时程序不会报错也不会停止。要避免出现死锁的现象。
- 同步的方式来操作共享资源,同一时间只有一个线程能够操作共享资源,降低了效率。
- wait()和notify()只能由锁对象调用,所以只能用在同步代码块中
四、synchronize和lock的异同:
synchronize在执行完相应的同步代码后会自动的释放同步代码块,lock需要手动的启动和手动的关闭