java线程安全
文章目录
一个概念:线程的同步(主要解决线程的安全问题)
问题:
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对一个内容的共享,会造成操作的不完整性,会破坏数据。
在Java中,通过同步机制来解决线程安全问题。
最初的两种方式:
方式一:同步代码块
synchronized(同步监视器){
//需要同步的代码
}
//说明:操作共享数据的代码,即为需要被同步的代码。
//共享数据:多个线程共同操作的变量。
//同步监视器:俗称:锁。任何一个类的对象都可以充当锁。要求:多个线程必须共用同一把锁。
示例代码:
同步代码块处理实现Runnable接口的线程安全问题:
public class SynchronizedTest {
public static void main(String[] args) {
RunnableTest runnableTest = new RunnableTest();
Thread thread = new Thread(runnableTest);
Thread thread1 = new Thread(runnableTest);
Thread thread2 = new Thread(runnableTest);
thread.start();
thread1.start();
thread2.start();
}
}
class RunnableTest implements Runnable{
private static int target = 100;
@Override
public void run() {
while (true){
synchronized(this){//在实现Runnable接口创建多线程的方式中,可以 考虑 使用this充当同步监视器。
if(target > 0){
System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
target--;
}else {
break;
}
}
}
}
}
同步代码块处理继承Thread类的线程安全问题:
public class ThreadTest {
public static void main(String[] args) {
ThreadClassTest test = new ThreadClassTest();
ThreadClassTest test1 = new ThreadClassTest();
ThreadClassTest test2 = new ThreadClassTest();
test.start();
test1.start();
test2.start();
}
}
class ThreadClassTest extends Thread{
private static int target = 100;
@Override
public void run() {
while (true){
synchronized(ThreadClassTest.class){
if(target > 0){
System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
target--;
}else {
break;
}
}
}
}
}
优点:解决了线程的安全问题
缺点:操作同步代码时只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。
这就是鱼与熊掌不可兼得吧。
方式二:同步方法
- 如果操作的共享数据完整的声明在一个方法中,就可以将此方法声明为同步的。
示例代码:
同步方法处理实现Runnable接口的线程安全问题:
public class SynchronizedTest1 {
public static void main(String[] args) {
RunnableTest1 runnableTest = new RunnableTest1();
Thread thread = new Thread(runnableTest);
Thread thread1 = new Thread(runnableTest);
Thread thread2 = new Thread(runnableTest);
thread.start();
thread1.start();
thread2.start();
}
}
class RunnableTest1 implements Runnable{
private int target = 100;
@Override
public void run() {
while (true){
show();
}
}
public synchronized void show(){// 默认使用的同步监视器是this
if(target > 0){
System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
target--;
}
}
}
同步方法处理继承Thread类的线程安全问题:
public class ThreadTest1 {
public static void main(String[] args) {
ThreadClassTest1 test = new ThreadClassTest1();
ThreadClassTest1 test1 = new ThreadClassTest1();
ThreadClassTest1 test2 = new ThreadClassTest1();
test.start();
test1.start();
test2.start();
}
}
class ThreadClassTest1 extends Thread{
private static int target = 100;
@Override
public void run() {
while (true){
show();
}
}
// public synchronized void show(){// 默认使用的同步监视器时this,有t1,t2,t3三个对象,所以错误
private static synchronized void show(){//同步监视器就是 ThreadClassTest1.class
if(target > 0){
System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
target--;
}
}
}
同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身(类.class)
线程安全之单例模式-懒汉式
同步方法的方式:
class User{
private User(){}
private static User user;
public static synchronized User getUser(){
if(user == null){
if(user == null){
user = new User();
}
}
return user;
}
}
使用这种方式不太好,因为不管user是不是以及实例化过,在这里全部单线程执行。
同步代码块的方式:
class User{
private User(){}
private static User user;
public static User getUser(){
if(user == null){
synchronized (User.class){
if(user == null){
user = new User();
}
}
}
return user;
}
}
这里用同步代码块更好,因为如果已经有实例对象了,就可以多线程执行了,全部拿到的同一个对象,不会一股脑阻塞在这里。效率更高
线程的死锁问题
用一段代码演示死锁问题:
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread(){
@Override
public void run() {
synchronized(s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(s2){
s1.append("b");
s2.append("2");
System.out.println(s1.toString());
System.out.println(s2.toString());
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized(s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(s1){
s1.append("d");
s2.append("4");
System.out.println(s1.toString());
System.out.println(s2.toString());
}
}
}
}).start();
}
在这个代码中第一个线程首先握住s1的锁,然后需要s2的锁才能执行下去,如果这个时候第二个线程握住了s2这个锁,同时需要s1的锁才能往下执行,两个线程所需要的锁都在对方手里,这样就形成了死锁。
官方一点的解释:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
我们使用同步时要避免死锁
避免死锁:
- 考虑专门的算法,避开这样的问题
- 尽量减少使用同步资源
- 尽量避免嵌套同步
方式三:Lock(锁)
jdk1.5提供的一个新特性。
Lock是一个接口,我们使用它的一个典型的实现类:ReentranLock类
示例代码:
Lock(锁)处理实现Runnable接口的线程安全问题:
public class LookTest {
public static void main(String[] args) {
LockTest lockTest = new LockTest();
Thread thread1 = new Thread(lockTest);
Thread thread2 = new Thread(lockTest);
Thread thread3 = new Thread(lockTest);
thread1.start();
thread2.start();
thread3.start();
}
}
class LockTest implements Runnable{
private int target = 100;
/**
* private ReentrantLock lock = new ReentrantLock(true);
* true表示公平,就是线程轮流执行,不会有那种插队的现象
* 无参构造器默认就是参数时false
* private ReentrantLock lock = new ReentrantLock();
*
*/
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
//调用锁定方法lock()
lock.lock();
if(target > 0){
System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
target--;
}else {
break;
}
}finally {
//调用解锁方法unlock()
lock.unlock();
}
}
}
}
synchronized和lock的异同
同:都可以解决线程安全问题
异:synchronized在执行完相应的同步代码块之后,自动的释放同步监视器,lock需要手动的去启动同步,同时手动结束同步。明显的lock更加的灵活
sleep() 和 wait()的异同
- 相同点
- 一旦执行方法,都可以使得当前线程进入阻塞状态
- 不同点
- 两个方法执行的位置不同,Thread类中声明sleep(),Object类中声明wait()
- 调用的要求不同,sleep()可以在任何需要的场景下调用,wait()只能使用在同步代码块或同步方法中。
- 如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放同步监视器(锁),wait()会释放锁。
新增线程创建方式
实现callable接口
jdk5.0新增的。
与Runable相比,Callable更强大些
- 相比run方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask,比如获取返回结果
Future接口:
- 可以对具体的Runable,callable任务的执行结果进行取消,查询是否完成,获取结果等。
- FutureTask是接口Future的唯一实现类
- FutureTask同时实现了Runnable,Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的值。
示例代码:
public class CallableTest {
public static void main(String[] args) {
//3.创建Callable接口实现类对象
NumTread numTread = new NumTread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(numTread);
//5.将FutureTask对象作为参数传递到Thread构造器中,创建Thread对象,并调用start()启动线程。
new Thread(futureTask).start();
try {
//6,获取Callable中call()方法的返回值(看需求)
//get()的返回值即为FutureTask构造器参数Callable实现类重写call()的返回值
Integer sum = futureTask.get();
System.out.println("总和为:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//1.创建一个callable的实现类,
class NumTread implements Callable<Integer>{
//2.实现call方法,将此线程需要执行的操作声明在call中
@Override
public Integer call() throws Exception {
int sum = 0 ;
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
使用线程池
背景:
经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响非常大。
解决办法:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放入池中。可以避免频繁创建销毁,实现重复利用。
好处:
- 提高响应速度(减少创建新新线程的时间)
- 降低资源消耗。(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
- 线程池大小
- 最大线程数
- 线程没有任务时最多保持多长时间终止。
- …
jdk1.5起提供了线程池相关的API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口,常见子类ThreadPoorExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T> task) :执行任务,有返回值,一般用来执行Callable
void shutdown():关闭连接池
- Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n);创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
示例代码:
public class ThreadPoolTest {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置连接池的属性
//强转为ExecutorService接口的实现类
// ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// service1.setCorePoolSize(15);
// service1.setMaximumPoolSize();
//执行指定线程的操作。需要实现Runnable接口或Callable接口实现类的对象
service.execute(new ThreadPoolGetNumber());//一般用来执行Runnable
service.execute(new ThreadPoolGetNumber());//一般用来执行Runnable
service.execute(new ThreadPoolGetNumber());//一般用来执行Runnable
// service.submit();//一般用来执行Callable
//关闭连接池
service.shutdown();
}
}
class ThreadPoolGetNumber implements Runnable{
private static Integer target = 100;
@Override
public void run() {
while (true){
synchronized(ThreadPoolGetNumber.class){
if(target > 0){
System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
target--;
}else {
break;
}
}
}
}
}
以上内容大部分是看康师傅视频时的笔记,详情请看:
尚硅谷康师傅,你值得拥有