文章目录
一、“脏数据”的产生
1、银行存钱的例子
2、原因
当多条语句在操作线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来。导致共享数据的错误。
3、解决方案
对多条操作共享数据的语句,只能让一个线程执行完。再执行过程中,其他线程不能够参与执行。
二、同步方案
1、同步代码块
2、同步方法
3、锁
在使用同步代码块方法时,
非静态方法可以使用 this 或者 Class 对象(字节码文件对象)
静态方法必须使用Class对象(字节码文件对象)来同步
三、使用snchroized关键字注意的点
1、snchroized 关键字不能被继承
2、在接口定义方法时不能使用snchroized 关键字
3、构造方法不能使用 snchroized 关键字,但是可以使用同步代码块来进行同步
4、可以自由放置 snchroized,不能放方法返回值之后
5、大量使用 snchroized 会造成 不必要的资源消耗和性能损失
6、表面上 snchroized 是锁定一个方法,实际上是锁定一个类。
例如 :method1和method2都使用 snchroized 关键字修饰,在method1未执行完之前,method2是不能执行的。
7、snchroized 不能同步变量
四、如何排查线程安全问题
1、明确哪些代码是多线程运行的代码
2、明确共享数据
3、明确多线程运行代码哪些语句是操作共享数据
五、利用同步 解决线程安全问题的 前提
1、必须要>=2 个线程
2、必须是多个线程使用同一个锁
六、利弊
1、优点: 解决了多线程安全问题
2、当线程获取到执行权的时候,每次都判断锁,消耗了资源(这个资源是在允许范围内的)
扩展 : 如何保证单例
一、脏数据的产生
1、银行存钱的例子
两个储户分别同时往银行存300元,每次存100,分三次存。
代码如下 :
//银行
public class Bank
{
private int sum = 0;
public void add(int n )
{
sum += n;
System.out.println(sum);
}
}
//用户
public class User implements Runnable
{
private Bank bank = new Bank();
@Override
public void run()
{
for(int x= 0 ; x < 3;x++)
{
bank.add(100);
}
}
}
// 在main 函数中
public static void main(String[] args)
{
//线程对象a
User a = new User();
Thread t1 = new Thread(a);
//线程对象b
User b = new User();
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
// 可能的输出结果
200
200
400
400
600
600
2、原因
两个线程同时访问共享数据 sum 。当第一条线 t1 程执行到 sum,并对sum进行数据操作时候,t1线程还未执行输出语句时候,失去执行权。t2线程对象获取执行权去执行sum数据操操作。同样的,还未执行到输出语句时候,t2 失去执行权。此刻t1 获取到执行权,执行输出语句。执行完成后,t1失去执行权,t2 获取到执行权,也同样执行输出语句。以此类推…
总结为:当多条语句在操作线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来。导致共享数据的错误。
如何模拟出现上述结果,其实也很简单,sleep()下即可。修改如下
public class Bank
{
private int sum = 0;
public void add(int n )
{
sum += n;
try
{
Thread.sleep(10);
}catch (InterruptedException e)
{
}
System.out.println(sum);
}
}
注意: 上述的 add方法是可以throws 异常,在run方法里面进行try-catch。 但是,run 方法不能throws 异常的。
3、解决方案
对多条操作共享数据的语句,只能让一个线程执行完。再执行过程中,其他线程不能够参与执行。
Java 对于多线程的安全问题提供了专业的解决方式。
二、同步方案
1、同步代码块
synchronized (对象)
{
//需要被同步的代码
}
//注意: 对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取到执行权,也进不去。
对add方法修改如下:
public class Bank
{
private int sum = 0;
Object obj = new Object();
public void add(int n )
{
// 同步代码块
synchronized (obj)
{
sum += n;
try
{
Thread.sleep(10);
}catch (InterruptedException e)
{
}
System.out.println(sum);
}
}
}
2、同步方法
在函数方法名前添加 synchronized 关键字 。
那么同步函数是哪一个锁呢 ? 函数需要被对象调用,那么函数都持有对象引用,就是this.所以同步函数使用的锁是 this。
如果同步函数被静态修饰后,使用的锁是什么 ? 该方法所在类的字节码文件对象,即 Class 对象。如何获取 ? 类名.class 或者 实例对象.getClass()
注意 : 非静态函数 同步的锁 可以是当前对象,也可以是非静态函数所在类的字节码文件对象。
修改如下:
public class Bank
{
private int sum = 0;
//Object obj = new Object();
public synchronized void add(int n )
{
//synchronized (obj)
//{
sum += n;
try
{
Thread.sleep(10);
}catch (InterruptedException e)
{
}
System.out.println(sum);
}
//}
}
三、使用snchroized关键字注意的点
1、snchroized 关键字不能被继承
如果在父类某个方法使用 snchroized 关键字,而自类覆盖了,子类默认情况是不同步的。如果子类方法也需要通保护,有两个方法,方法名前添加 snchroized 或者 在方法体内 调用父类方法。
2、在接口定义方法时不能使用snchroized 关键字
3、构造方法不能使用 snchroized 关键字,但是可以使用同步代码块来进行同步
4、可以自由放置 snchroized
snchroized 不能放在 方法返回值类型后面 !!!
5、大量使用 snchroized 会造成 不必要的资源消耗和性能损失
四、如何查找线程安全问题
1、明确哪些代码是多线程运行的代码 即 run方法
2.、明确共享数据 sum
3、明确多线程运行代码哪些语句是操作共享数据 sim += n 和 输出语句
五、利用同步解决线程安全问题的的前提
1、必须要>=2 个线程
2、必须是多个线程使用同一个锁
六、同步的利弊如下
优点: 解决了多线程安全问题
弊端:当线程获取到执行权的时候,每次都判断锁,消耗了资源(这个资源是在允许范围内的)
多线程在单例中的运用