多线程安全问题

一、创建线程

1、继承Thread类,重写run()方法

Thread类实现了Runnable接口。
创建线程需要继承Thread类,存在单继承问题。此外,Thread类将线程类和线程任务耦合到了一起,不符合java思想。推荐使用第二种。
线程类

public class MyThread extends Thread {
   //定义指定线程名称的构造方法
   public MyThread(String name) {
   	//调用父类的String参数的构造方法,指定线程的名称
   	super(name);
   }
   /**
    * 重写run方法,完成该线程执行的逻辑
    */
   @Override
   public void run() {
   	for (int i = 0; i < 10; i++) {
   		System.out.println(getName()+":正在执行!"+i);
   	}
   }
}

主类

public class Demo01 {
	public static void main(String[] args) {
		//创建自定义线程对象
		MyThread mt = new MyThread("新的线程!");
		//开启新线程
		mt.start();
		//在主方法中执行for循环
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程!"+i);
		}
	}
}

2、实现Runnable接口,重写run()方法

此种方法将线程类和线程任务分开了,降低了耦合度。推荐使用。

public class MyRunnable implements Runnable{

	//定义线程要执行的run方法逻辑
	@Override
	public void run() {
		
		for (int i = 0; i < 10; i++) {
			System.out.println("我的线程:正在执行!"+i);
		}
	}
}
public class Demo02 {
	public static void main(String[] args) {
		//创建线程执行目标类对象
		Runnable runn = new MyRunnable();
		//将Runnable接口的子类对象作为参数传递给Thread类的构造函数
		Thread thread = new Thread(runn);
		Thread thread2 = new Thread(runn);
		//开启线程
		thread.start();
		thread2.start();
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程:正在执行!"+i);
		}
	}
}

3、Callable接口

与Runnable接口功能相似,用来指定线程的任务,但可以返回数据。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。

4、线程池

  • ExecutorService:线程池类
  • Future submit(Callable task):获取线程池中的某一个线程对象,并执行线程中的call()方法
  • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
public class ThreadPoolDemo {
	public static void main(String[] args) {
		//创建线程池对象
		ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
		//创建Callable对象
		MyCallable c = new MyCallable();
		
		//从线程池中获取线程对象,然后调用MyRunnable中的run()
		service.submit(c);
		
		//再获取个教练
		service.submit(c);
		service.submit(c);
//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中

//关闭线程池
		//service.shutdown();
	}
}
public class MyCallable implements Callable {
	@Override
	public Object call() throws Exception {
		System.out.println("我要一个教练:call");
		Thread.sleep(2000);
		System.out.println("教练来了: " +Thread.currentThread().getName());
		System.out.println("教我游泳,交完后,教练回到了游泳池");
		return null;
	}
}

二、线程安全问题

1、同步代码块

当多个线程同时操作全局变量或局部变量时便可能出现线程问题。java提供了synchronized关键字来解决这个问题。
同步代码块: 在代码块声明上 加上synchronized

synchronized (锁对象) {
	可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
我们可以将synchronized理解为给代码块加了一把锁,锁对象是一把钥匙。当线程进入同步代码块时,会取走钥匙,直到执行完代码块才会返还钥匙。在未返回钥匙之前,其他线程应为没有钥匙都进不了同步代码块。
代码示例:

//lock为一个随便传入的对象,注意不能传入匿名对象
synchronized (lock){
	if (ticket > 0) {
		//模拟电影选坐的操作
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
	}
}

synchronized关键字能将括号内的代码变成一个整体,不管内部是否休眠。它一旦开始执行,便必须将整个代码块都执行完。synchronized能提高安全性,但它也降低了程序的运行速度。

2、同步方法

同步方法是将同步代码块抽取成一个方法,在方法上加synchronized关键字,效果和同步代码块一样。
同步方法:在方法声明上加上synchronized

public synchronized void method(){
   	可能会产生线程安全问题的代码
}

同步方法中,非静态方法的同步锁对象是this,即对象本身;静态方法中,同步锁对象是类.class。

3、Lock接口

当同步代码块中发生异常后,线程不能往后执行,便不能返还同步锁,此时便会出问题。所以java还提供了Lock接口。
Lock接口中的常用方法

  • void lock() 获取锁
  • void unlock() 释放锁
public class Ticket implements Runnable {
	//共100票
	int ticket = 100;
	
	//创建Lock锁对象,ReentrantLock为Lock接口实现类
	Lock ck = new ReentrantLock();
	
	@Override
	public void run() {
		//模拟卖票
		while(true){
			//synchronized (lock){
			ck.lock();
				if (ticket > 0) {
					//模拟选坐的操作
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
				}
			ck.unlock();
			//}
		}
	}
}

4、死锁

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。
代码演示
定义锁对象类

public class MyLock {
	public static final Object lockA = new Object();
	public static final Object lockB = new Object();
}

线程任务类

public class ThreadTask implements Runnable {
	int x = new Random().nextInt(1);//0,1
	//指定线程要执行的任务代码
	@Override
	public void run() {
		while(true){
			if (x%2 ==0) {
				//情况一
				synchronized (MyLock.lockA) {
					System.out.println("if-LockA");
					synchronized (MyLock.lockB) {
						System.out.println("if-LockB");
						System.out.println("if大口吃肉");
					}
				}
			} else {
				//情况二
				synchronized (MyLock.lockB) {
					System.out.println("else-LockB");
					synchronized (MyLock.lockA) {
						System.out.println("else-LockA");
						System.out.println("else大口吃肉");
					}
				}
			}
			x++;
		}
	}
}

测试类

public class ThreadDemo {
	public static void main(String[] args) {
		//创建线程任务类对象
		ThreadTask task = new ThreadTask();
		//创建两个线程
		Thread t1 = new Thread(task);
		Thread t2 = new Thread(task);
		//启动线程
		t1.start();
		t2.start();
	}
}

5、等待唤醒机制

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制所涉及到的方法:

  • wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
  • notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
  • notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
代码演示:
模拟资源类

public class Resource {
	private String name;
	private String sex;
	private boolean flag = false;

	public synchronized void set(String name, String sex) {
		if (flag)
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		// 设置成员变量
		this.name = name;
		this.sex = sex;
		// 设置之后,Resource中有值,将标记该为 true ,
		flag = true;
		// 唤醒output
		this.notify();
	}

	public synchronized void out() {
		if (!flag)
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		// 输出线程将数据输出
		System.out.println("姓名: " + name + ",性别: " + sex);
		// 改变标记,以便输入线程输入数据
		flag = false;
		// 唤醒input,进行数据输入
		this.notify();
	}
}

输入线程任务类

public class Input implements Runnable {
	private Resource r;

	public Input(Resource r) {
		this.r = r;
	}

	@Override
	public void run() {
		int count = 0;
		while (true) {
			if (count == 0) {
				r.set("小明", "男生");
			} else {
				r.set("小花", "女生");
			}
			// 在两个数据之间进行切换
			count = (count + 1) % 2;
		}
	}
}

输出线程任务类

public class Output implements Runnable {
	private Resource r;

	public Output(Resource r) {
		this.r = r;
	}

	@Override
	public void run() {
		while (true) {
			r.out();
		}
	}
}

测试类

public class ResourceDemo {
	public static void main(String[] args) {
		// 资源对象
		Resource r = new Resource();
		// 任务对象
		Input in = new Input(r);
		Output out = new Output(r);
		// 线程对象
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		// 开启线程
		t1.start();
		t2.start();
	}
}

6、sleep()和wait()方法的区别

  • sleep: 不释放锁对象, 释放CPU使用权
    • 在休眠的时间内,不能唤醒
  • wait(): 释放锁对象, 释放CPU使用权
    • 在等待的时间内,能唤醒
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值