一.多线程的安全问题
1.多线程安全问题发生场景:
承接上篇博客的内容,使用Runnable接口创建线程
此时,int类型的数据num是一个共享数据,被多个线程一起调用
//毕向东经典买票程序
class Ticket implements Runnable{
private int num = 100;
public void run(){
while(true){
if(num>0){
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
}
}
}
class TicketDemo{
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//t1,t2,t3,t4四个线程同时调用共享数据num
t1.start();
t2.start();
t3.start();
t4.start();
}
}
可能会出现如图所示的数据错误(如果图片看不清,请将图片下载后用看图工具放大后查看)
如上图所示,若线程二执行到tickets–(–操作未执行)进入等待状态,执行线程一,假设此时变量tickets的值为1,则执行if判断通过,接着执行tickets–,执行完后tickets值为0,如果这时CPU突然切换到线程二,执行tickets–操作,则执行完毕后tickets的值为-1,数据出错
虽然这种情况发生的几率非常小,但是,一旦发生,就是不可挽回的错误,而且很难被发现,所以必须想办法避免
2.多线程安全问题的原因:
一个线程在执行时,运算一个共享数据,在执行过程中,其他线程参与进来,并操作了这个数据。导致到了错误数据的产生。
涉及到两个因素:
1,多个线程在操作一个共享数据
2,有多条语句对共享数据进行运算
3.多线程安全问题的解决方案:
将操作共享数据的语句在某一时段让一个线程执行直到线程任务结束,在执行过程中,其他线程不能进来执行
java中提供了同步代码块的方式进行对操作共享数据的代码的封装
格式:
synchronized(对象) {
// synchronized中传入的对象可以是任意对象,这个对象就是锁
需要被同步的代码;
}
使用同步代码块进行同步之后的买票程序:
class Ticket implements Runnable//extends Thread
{
private int num = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)
{
if(num>0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
二.同步
优势:解决了线程安全问题
弊端:降低部分性能,因为判断锁需要消耗资源,而且带来了死锁问题
1.定义同步的前提
1,必须要有两个或者两个以上的线程,才需要同步。
2,多个线程必须保证使用的是同一个锁。
2.同步的第二种表现形式
//毕向东经典银行存钱程序
/**
*两个客户向银行存钱,没人每次存100,各存3次
*sum是共享数据,多个线程对sum操作可能使sum的值出错
*Bank类的add()方法中,有两步操作,1,sum累和。2,sum输出,正常情况下程序运行,应该输出sum=100至600,但是可能会出现输出重复值,比如输出两个300的情况
*错误分析:若线程一执行sum累和后sum值为200,该执行输出sum,但是CPU切换到线程二执行sum累和,执行完后sum值为300,然后CPU再次切换到线程一,输出sum=300(正常情况线程一应该输出sum=200),程序出错
*/
class Bank{
private int sum;
public void add(int num){
sum = sum + num;
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println("sum="+sum);
}
}
class Cus implements Runnable{
private Bank b = new Bank();
public void run(){
for(int x=0; x<3; x++){
b.add(100);
}
}
}
class BankDemo {
public static void main(String[] args){
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
同步函数:
其实就是将同步关键字定义在函数上,让函数具备了同步性
使用同步函数后:
class Bank{
private int sum;
//同步函数
public synchronized void add(int num){
sum = sum + num;
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println("sum="+sum);
}
}
class Cus implements Runnable{
private Bank b = new Bank();
public void run(){
for(int x=0; x<3; x++){
b.add(100);
}
}
}
class BankDemo {
public static void main(String[] args){
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
同步函数的锁
验证同步函数锁的程序:
//毕向东经典测试同步函数锁程序
class Ticket implements Runnable{
private int num = 100;
boolean flag = true;
//实现run()方法
public void run(){
if(flag)
while(true){
//定义一个同步代码块,当flag=true时执行
synchronized(this){
if(num>0){
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....obj...."+num--);
}
}
}
else
while(true)
//当flag=false时执行show()函数(同步函数)
this.show();
}
//定义一个同步函数,show()函数
public synchronized void show(){
if(num>0){
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....function...."+num--);
}
}
}
class SynFunctionLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
//定义两个线程,t1线程开启时flag=true,t1线程执行同步代码块中的内容
t1.start();
/**
*设置主线程休息10毫秒,让t1线程带着参数flag=true执行,如果不设置主线程休息,
*则主线程会一直执行下去,设置flag的值为false,如果t1线程没有及时开启,则t1线程执行时flag的值就会被被设置为false,
*也去执行同步函数中的内容了,达不到预期的两个线程分别执行同步函数和同步代码块的效果
*/
try{Thread.sleep(10);}catch(InterruptedException e){}
t.flag = false;
//t2线程开启时flag=false,t2线程执行同步函数中的内容
t2.start();
}
}
上述代码执行,如果多次执行后没有出现多线程安全问题,说明同步函数和同步代码块使用的是同一把锁,实现了买票代码的同步
通过验证,同步函数使用的锁就是this锁,即使用自己所在的对象this对象作为同步的锁
当同步函数被static修饰时
静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了字节码文件对象。而静态同步函数就是使用同这个字节码文件对象(类名.class)
3.同步代码块和同步函数的区别
1.锁对象不同
同步代码块使用的锁可以是任意对象。
同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。
2.适用场景不同
如果一个类中只有一个同步,可以使用同步函数
如果有多同步,必须使用同步代码块,来确定不同的锁。同步代码块相对灵活一些
引申:多线程在单例设计模式中的使用
饿汉式:
class Single{
privatestatic Single s = new Single();
privateSingle(){}
publicstatic Single getInstance()
{
return s;
}
}
懒汉式:
class Single {
privatestatic static Single s = null;
privateSingle(){}
publicstatic static Single getInstance() {
if(s==null)
s= new Single();
return s;
}
}
当多线程访问懒汉式时,因为懒汉式的方法内对共享数据(s)进行多条语句的操作(if判断和创建对象),所以容易出现线程安全问题。为了解决线程安全问题,加入同步机制
懒汉式:(使用同步函数同步)
class Single {
privatestatic static Single s = null;
privateSingle(){}
publicstatic static synchronized Single getInstance() {
if(s==null)
s= new Single();
return s;
}
}
使用同步函数后程序的效率又降低了,因为当Single对象创建之后,如果其他对象调用单例程序,则需要两次判断(先判断锁,再进行一次if判断)
为解决效率降低问题,使用双重判断
懒汉式:(使用同步代码块加双重判断)
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s == null){
//锁是字节码文件对象
synchronized(Single.class){
if(s == null)
s = new Single();
}
}
return s;
}
}
三.死锁
发生在同步的嵌套中,同步函数中有同步代码块,同步代码块中有同步函数
/**
*死锁:常见情景之一:同步的嵌套。
*使用验证同步函数锁的程序稍加调整改为一个死锁程序
*/
class Ticket implements Runnable{
private int num = 100;
Object obj = new Object();
boolean flag = true;
public void run(){
//flag=ture时执行
if(flag)
while(true){
//先执行同步代码块,锁对象是obj
synchronized(obj){
//再执行同步函数,锁对象是一个Ticket类的对象(即show函数所在的对象)
show();
}
}
//flag=false时执行
else
while(true)
//先执行同步函数(this锁),再执行同步代码块(obj锁)
this.show();
}
//同步函数
public synchronized void show(){
//同步代码块
synchronized(obj){
if(num>0){
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
}
}
}
class DeadLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(InterruptedException e){}
t.flag = false;
t2.start();
}
}
一个更为简单直接的死锁程序:
class Test implements Runnable{
private boolean flag;
Test(boolean flag){
this.flag = flag;
}
public void run(){
if(flag){
while(true)
synchronized(MyLock.locka){
System.out.println(Thread.currentThread().getName()+"..if locka....");
synchronized(MyLock.lockb) {
System.out.println(Thread.currentThread().getName()+"..if lockb....");
}
}
}
else{
while(true)
synchronized(MyLock.lockb){
System.out.println(Thread.currentThread().getName()+"..else lockb....");
synchronized(MyLock.locka){
System.out.println(Thread.currentThread().getName()+"..else locka....");
}
}
}
}
}
class MyLock{
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
class DeadLockTest {
public static void main(String[] args) {
Test a = new Test(true);
Test b = new Test(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
}