java线程(2)-线程同步
本节主要是在前面吃苹果的基础上发现问题,然后提出三种解决方式
1.线程不安全问题
什么叫线程不安全呢
即当多线程并发访问同一个资源对象的时候,可能出现不安全的问题
对于前一章例子中,使用接口实现方式时会有重复现象,使用接口方式时我们还没有发现明显的现象,但是这并不代表原来的代码没有问题
我们发现没有问题,必须要有这个意识:看不到问题,有可能是我们经验太少,或者问题出现的不够明显。
如果问题不够明显,我们可以使用Thread.sleep()方法,正在运行的线程暂停,此时会执行另外的进程,如此可以更方便的看到问题
注意:
在线程的run方法上不能使用throws来声明抛出异常,只能在方法中使用try-catch来处理异常
原因是:子类覆盖父类的原则,子类不能抛出新的异常。
在Runnable接口中的run方法,都没有抛出异常,所以子类中也不能对这个方法抛出异常
父类中:public abstract void run();
解决方法的原理
回到我们用继承方法的问题上来,如何才能让一个线程没有执行完时另一个线程不会操作一些动作呢。
解决方案是:保证某一些的动作的同步完成。即保证打印苹果和苹果总数-1操作,必须同步,作为一个单元来执行
具体的解决方法有3个:
方式1:同步代码块
方式2:同步方法
方式3:锁机制(lock)
下面分别实现三种方法
2.线程同步
思想是把不能中断或者分开执行的代码绑定在一起执行,使其不能在执行中中断
2.1.同步代码块
语法:
synchronized(同步锁)
{
需要同步操作的代码
}
同步锁:
为了保证没一个线程都能正常执行原子操作,java引入了线程同步机制
同步锁又被称为:同步监听对象/同步监听器/互斥锁
其实就是相当与一把只有一个坑位的卫生间门上的锁,当里面有人的时候,门必须要被锁上,等人出来了再把锁打开
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁
java程序运行使用任何对象作为同步锁,但是一般的,我们是用当前并发访问的共同资源作为同步监听对象
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他线程,只能在外面等着
示例代码:
//接口实现方式
class Apple1 implements Runnable{
private int num = 50;
public void run(){
for (int i = 0; i < 50; i++) {
synchronized (this){
if (num > 0){
System.out.println(Thread.currentThread().getName()+"吃了编号为"+ num +"的苹果");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
}
}
}
}
}
public class appleImplements {
public static void main(String[] args) {
//创建桑个线程
Apple1 a = new Apple1();
new Thread(a,"小A").start();
new Thread(a,"小B").start();
new Thread(a,"小C").start();
}
}
注意:因为继承方法没有不能实现资源的共享,所以这里的例子都是以接口方式实现的。
2.2.同步方法
同步方法:使用synchronized修饰的方法,就叫做同步方法。保证A线程执行该方法的时候,其他线程只能在外面等着
语法:
synchronized public void doWork(){
//TODO 要执行的动作
}
同步锁是谁:
对于非static方法,同步锁就是this
对于static方法,我们使用当前方法所在的类的字节码对象(Apple2.class)
注意:不要使用synchronized修饰run方法,修饰之后,某一个线程就执行完了所有的功能,好比是多个线程出现串行
代码实现:
//接口实现方式
class Apple2 implements Runnable{
private int num = 500;
public void run(){
for (int i = 0; i < 500; i++) {
eat();
}
}
synchronized private void eat(){
if (num > 0){
System.out.println(Thread.currentThread().getName()+"吃了编号为"+ num +"的苹果");
try {
Thread.sleep(10);//模拟网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
}
}
}
public class appleImplements {
public static void main(String[] args) {
//创建桑个线程
Apple2 a = new Apple2();
new Thread(a,"小A").start();
new Thread(a,"小B").start();
new Thread(a,"小C").start();
}
}
2.3.同步锁(Lock)
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象
语法:
思想就是,在执行要绑定的代码之前,上一把锁,执行完之后呢,就把锁打开,这样别的线程就能继续了。
在使用这个锁之前呢,要先new一个锁出来
代码示例;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//接口实现方式
class Apple4 implements Runnable{
private int num = 50;
private final Lock lock = new ReentrantLock();//创建一个锁对象
public void run(){
for (int i = 0; i < 50; i++) {
eat();
}
}
private void eat(){
//进入方法:立马加锁
lock.lock();//获取锁
if (num > 0){
try {
System.out.println(Thread.currentThread().getName()+"吃了编号为"+ num +"的苹果");
Thread.sleep(100);//模拟网络延迟
num--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//执行完,走人,并把锁打开
lock.unlock();//释放锁
}
}
}
}
public class appleImplements {
public static void main(String[] args) {
//创建桑个线程
Apple4 a = new Apple4();
new Thread(a,"小A").start();
new Thread(a,"小B").start();
new Thread(a,"小C").start();
}
}
2.4.synchronized的好与坏
好处:保证了多线程并并发访问时的同步操作,避免线程的安全性问题
缺点:使用synchronized方法/代码块的性能比不用要低一些
建议:尽量减小synchronized的作用域