线程安全问题
我们知道多个线程共享堆内存,当两个或者多个线程调用同一个对象的方法操作对象成员时,因为cpu轮流执行线程,线程A刚开始操作对象方法,修改了数据,轮到线程B运行,线程B也操作对象方法,修改数据,可能又轮到线程A操作对象方法,接着上次线程A的剩余部分执行,那这个时候的数据是被线程B修改后的数据,这样会造成线程操作数据出错,这叫做异步处理,因为谁也不知道现在的数据是被线程A修改后的数据,还是被线程B修改后的数据,会造成线程的不安全
我们人为地在方法前面加上synchronizd关键字,当线程A调用对象方法时,会在这个方法加锁,轮到线程B调用对象方法时,线程B访问不到该方法,这叫同步锁,只有线程A执行完这个方法时,锁才会打开,线程B才能访问该方法,好像我们排队买车票,售票员看了下剩余车票,你选了一张,正在付款,但还没完成,这时第二个人也去买这趟车的车票,则第二个售票员不能把你正在付款的这张车票卖给第二个人,这叫同步
如图所示:
异步问题如上图所示,说明: 如果线程t1先执行s.setAge 方法,将s的age设为12,那么可能setAge方法中还有其他的语句还没执行完,就开始t2线程执行,将age设置为50,而接着该t1线程执行时,接着setAge方法执行,那里面的语句读到的age可能已经是被t2线程修改过的age编程50了,就出现了线程安全问题
线程不安全的代码示例:
public class synchronizedTest01 {
public static void main(String[] args){
Student s=new Student();
Thread t1=new Thread("线程t1"){
@Override
public void run() {
s.setAge(12);
System.out.println(this.getName()+" : "+s.getAge());
}
};
Thread t2=new Thread("线程t2"){
@Override
public void run() {
s.setAge(50);
System.out.println(this.getName()+" : "+s.getAge());
}
};
t1.start();
t2.start();
}
}
class Student{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
System.out.println( "the Student's age is -----begin");
System.out.println( "the Student's age is -------"+this.age);
System.out.println( "the Student's age is ----end");
}
}
out:
the Student's age is -----begin
the Student's age is -----begin
the Student's age is -------12
the Student's age is ----end
the Student's age is -------12
the Student's age is ----end
线程t2 : 12
线程t1 : 12
在方法前加上synchronized关键字修饰,这样在执行多线程时,要看哪个线程先执行这个方法,假设有A,B,C三个线程都调用了同一个对象的方法F,A先执行了这个方法,那么线程A就会在这个对象的F方法上加锁,别的线程无法执行这个对象上的F方法,直到A执行这个方法结束,B,C两个线程中的一个线程才能执行这个F方法,保证了某个时间段内只有一个线程执行F方法,解决了线程安全问题,synchronized锁住的是当前对象,当不同线程操作不同对象的时候,不存在线程安全问题,因为他们各自操作堆内存中的不同对象
用synchronized修饰的方法就是同步方法,被线程调用时锁住该方法,只有这个线程执行完这个方法才能解锁,被其他线程调用,再锁住执行完后再开锁
以上述安全问题为例来演示同步方法的使用:
package synchronizedTest;
public class synchronizedTest02 {
public static void main(String[] args){
Student1 s=new Student1();
Thread t1=new Thread("线程t1"){
@Override
public void run() {
s.setAge(12);
System.out.println(this.getName()+": "+s.getAge());
}
};
Thread t2=new Thread("线程t2"){
@Override
public void run() {
s.setAge(50);
System.out.println(this.getName()+": "+s.getAge());
}
};
t1.start();
t2.start();
}
}
class Student1{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public synchronized void setAge(int age) {
this.age = age;
System.out.println( "the Student's age is -----begin");
System.out.println( "the Student's age is -------"+this.age);
System.out.println( "the Student's age is ----end");
}
}
out:
the Student's age is -----begin
the Student's age is -------12
the Student's age is ----end
the Student's age is -----begin
the Student's age is -------50
the Student's age is ----end
线程t2: 50
线程t1: 50
看到CPU会轮流随机执行各个线程,当线程体中执行一个对象的方法时,会在setAge上加锁,该方法不能被其他线程访问,当线程A执行完setAge方法时,才能被另一个线程调用这个方法,也是要加锁,保证这段时间只有这个线程能执行该方法,cpu再轮流随机执行线程体其他内容,不确定先执行哪个线程体内容
如果同步方法中有一段耗时较长的代码,又不涉及到线程安全问题,使用synchronized修饰方法变成同步方法就不合适了,那咱们 只让涉及到线程安全的代码部分变成线程安全的就可以一定避免耗时过长的问题,由此引出了同步代码块的概念
同步代码块:将需要同步的代码放到synchronized代码块中,因为那段耗时较长的代码是在异步情况下运行,所以可以节省一些时间。
同步代码块的语法:
class A{
Object obj=new Object();
function(){
synchronized(obj){
语句1;
语句2;
}
}
}
注意:同步代码块中的obj对象可以是任意对象,但要保证多个线程调用这个方法时,使用的obj对象是同一个,比如obj对象在方法体外部创建,多个线程调用这个方法时,obj对象就是同一个.
如果obj对象是在方法中创建的,那么不同线程调用这个对象的function方法时,各自创建了一个obj对象,会出现两把锁,锁不住这段代码,还会导致线程安全的问题,如下代码形式就是调用了不同的obj,还会导致出现线程安全问题
class A{
function(){
Obj obj=new Obj();
synchronized(obj){
同步代码块的语句;
}
}
}
画图说明如下:
假设t1先执行同步代码块内容,会尝试获取obj对象的monitor所有权,获取成功后,t1开始执行同步代码块中方法,在执行过程中t2/t3要执行同步代码块内容,也要尝试获取obj对象的所有权,但此时t1控制了obj的所有权,还没有执行完同步代码块内容,t2/t3获取不到,等t1执行完同步代码块内容后,放开obj的所有权,t2/t3再去尝试获取monitor所有权的时候才可能成功
假设t1先执行同步代码块内容,获取monitor所有权,t1还没执行完同步代码块内容,此时t2也要执行同步代码块内容,获取另一个obj对象对应的monitor的所有权,获取成功,这样就达不到同步锁的效果
好啦,下面演示同步代码块的使用:
package synchronizedTest;
public class synchronizedTest {
public static void main(String[] args){
Student2 s=new Student2();
Thread t1=new Thread("线程t1"){
@Override
public void run() {
s.setAge(12);
System.out.println(this.getName()+" : "+s.getAge());
}
};
Thread t2=new Thread("线程t2"){
@Override
public void run() {
s.setAge(50);
System.out.println(this.getName()+" : "+s.getAge());
}
};
t1.start();
t2.start();
}
}
class Student2{
private int age;
Object obj=new Object();
//该方法可以抛出异常,继承Thread后重写的run方法不能抛出异常,只能捕获
public void setAge(int age) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
if (age > 0 && age < 200) {
this.age = age;
System.out.println("begin------------");
System.out.println(Thread.currentThread().getName() + " : " + this.age);
System.out.println("end----------------");
}
}
}
public int getAge() {
return age;
}
}
out:
begin------------
线程t1 : 12
end----------------
begin------------
线程t2 : 50
end----------------
线程t1 : 50
线程t2 : 50
说明,试用同步代码块保证了代码块内的代码是同步执行的,同步代码块外面的代码是异步执行的上述代码没有测试时间,下面贴上测试同步代码块消耗时间的代码:
public class synchronizedTest {
static long start1;
static long start2;
static long end1;
static long end2;
public static void main(String[] args){
Student2 s=new Student2();
Thread t1=new Thread("线程t1"){
@Override
public void run() {
start1=System.currentTimeMillis();
s.setAge(12);
System.out.println(this.getName()+" : "+s.getAge());
end1=System.currentTimeMillis();
}
};
Thread t2=new Thread("线程t2"){
@Override
public void run() {
start2=System.currentTimeMillis();
s.setAge(50);
System.out.println(this.getName()+" : "+s.getAge());
end2=System.currentTimeMillis();
}
};
t1.start();
t2.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long start;
long end;
if (start1 0 && age < 200) {
this.age = age;
System.out.println("begin------------");
System.out.println(Thread.currentThread().getName() + " : " + this.age);
System.out.println("end----------------");
}
}
}
public int getAge() {
return age;
}
}
out:
begin------------
线程t1 : 12
end----------------
begin------------
线程t2 : 50
end----------------
线程t2 : 50
线程t1 : 12
同步代码块耗时5秒
以下代码测试同步方法消耗时间
package synchronizedTest;
public class synchronizedTest04 {
static long start1;
static long start2;
static long end1;
static long end2;
public static void main(String[] args){
Student3 s=new Student3();
Thread t1=new Thread("线程t1"){
@Override
public void run() {
start1=System.currentTimeMillis();
s.setAge(12);
System.out.println(this.getName()+" : "+s.getAge());
end1=System.currentTimeMillis();
}
};
Thread t2=new Thread("线程t2"){
@Override
public void run() {
start2=System.currentTimeMillis();
s.setAge(50);
System.out.println(this.getName()+" : "+s.getAge());
end2=System.currentTimeMillis();
}
};
t1.start();
t2.start();
try {//让主线程睡眠时间大于两个线程消耗时间才能测量出来两个线程消耗的时间
//因为当A线程睡眠时,cpu调用B线程或者主线程,用到主线程时让其睡眠多一点,用到B线程时
//因为锁住了同步方法,所以B线程只能等待,等A线程睡眠完成后,cpu再调度,主线程还在睡眠,
// 只能从线程B或者线程A剩余的部分中执行,无论执行这两个中那个,都能准确计算出来两个线程共同消耗的时间
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long start;
long end;
if (start1 0 && age < 200) {
this.age = age;
System.out.println("begin------------");
System.out.println(Thread.currentThread().getName() + " : " + this.age);
System.out.println("end----------------");
}
}
public int getAge() {
return age;
}
}
out:
begin------------
线程t1 : 12
end----------------
线程t1 : 12
begin------------
线程t2 : 50
end----------------
线程t2 : 50
同步方法耗时10秒
有循环体的方法,使用同步方法时要慎重,如果多个线程都调用这个方法,那么当其中一个线程进入循环体后不达到循环体结束条件是不会退出的,这是可以使用同步代码块,如下
下面来接着做上次的练习,2个线程共同打印0-100内容
public class synchronizedTest05 {
public static void main(String[] args){
MutliPrint mp=new MutliPrint();
Thread t1=new Thread(mp,"线程1");
Thread t2=new Thread(mp,"线程2");
t1.start();
t2.start();
}
}
class MutliPrint implements Runnable{
static int i=0;
@Override
public synchronized void run() {
while(i<100) {
System.out.println(Thread.currentThread().getName()+" : " + (i++));
Thread.yield();
}
}
}
out:
结果正确,但太长,就不贴了
练习:三个公司共同卖100张电影票,
package synchronizedTest;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;
//开启三个线程
public class synchronizedTest06 {
public static void main(String[] args){
Ticket tc=new Ticket();
Thread t1=new Thread(tc,"美团电影");
Thread t2=new Thread(tc,"淘票票");
Thread t3=new Thread(tc,"猫眼电影");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable{
//100张电影票
ArrayList ticketArr;
Random random;
public Ticket(){
ticketArr=new ArrayList<>();
random=new Random();
for (int i=0;i<100;i++){
ticketArr.add(i+1);
}
}
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticketArr.size()<=0){
break;
}
int j = random.nextInt(100) + 1;
if (ticketArr.contains(j)) {
//利用循环迭代来删除List中的元素,直接删除可能会报错,因为删除元素后List元素的索引会变化,所以不能只用用索引来删除
Iterator it = ticketArr.iterator();
while (it.hasNext()) {
if (it.next().equals(j)) {
it.remove();
break;
}
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + j + "张票,还剩余" + ticketArr.size() + "张票");
}
}
}
}
}
参考:http://www.monkey1024.com/javase/676