1.为什么是重点?
以后在开发中,项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动等,都已经实现完了。这些代码都不需要编写,最重要的是要知道:编写的程序需要放到一个多线程的环境下运行,更需要关注这些数据在多线程并发的环境下是否是安全的。
2.什么时候数据在多线程并发的环境下会存在安全问题?
三个条件:
(1)条件1:多线程并发
(2)条件2:多线程有共享的数据
(3)条件3:共享的数据有修改的行为
满足以上3个条件之后,就会存在线程安全问题。
3.如何解决线程安全问题?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题。
此时应当使线程排队执行(不能并发)。用排队执行解决线程安全问题,这种机制称为:线程同步机制。(这是专业术语的叫法,实际上就是线程不能并发了,必须排队执行)。线程同步(也就是线程排队)会牺牲一部分效率,但是数据安全是第一位的。
4.线程同步涉及到的两个专业术语
4.1 异步编程模型
线程t1和t2各自执行各自的,相互不管,谁也不需要等谁。(其实就是多线程并发,效率较高)
4.2 同步编程模型
线程t1和t2,在某一线程执行的时候,另一个线程必须等待正在执行的线程,一直到其执行完毕为止。两个线程之间发生了等待关系,线程排队执行,效率较低。
5.例子分析
1 package thread_safe; 2 3 public class Account { 4 5 //账户 6 private String sctno; 7 8 //余额 9 private double balance; 10 11 public Account() { 12 13 } 14 15 public Account(String sctno, double balance) { 16 this.sctno = sctno; 17 this.balance = balance; 18 } 19 20 public String getSctno() { 21 return sctno; 22 } 23 24 public void setSctno(String sctno) { 25 this.sctno = sctno; 26 } 27 28 public double getBalance() { 29 return balance; 30 } 31 32 public void setBalance(double balance) { 33 this.balance = balance; 34 } 35 36 //取款方法 37 public void withdraw(double money){ 38 //t1和t2并发这个方法;t1、t2是两个栈。两个栈操作堆中同一个对象 39 //取款前的余额 40 double before=this.getBalance(); 41 double after=before-money;//取款后的余额 42 //更新余额 43 //若t1执行到这里,但还没来得及执行第44行代码,t2线程进来withdraw()方法了,此时一定出现问题。 44 this.setBalance(after); 45 46 } 47 48 }
1 package thread_safe; 2 3 public class AccountThread extends Thread { 4 5 //两个线程必须共享同一个账户对象 6 private Account act; 7 8 //通过构造方法传递过来账户对象 9 public AccountThread(Account act){ 10 this.act=act; 11 } 12 public void run(){ 13 //run()方法执行取款操作 14 //假设取款5000 15 double money=5000; 16 //取款 17 act.withdraw(money); 18 System.out.println(Thread.currentThread().getName()+"对账户"+act.getSctno()+"取款成功,余额:"+act.getBalance()); 19 } 20 }
1 package thread_safe; 2 3 public class Test { 4 public static void main(String[] args){ 5 6 //创建一个账户对象 7 Account act=new Account("act-001",10000); 8 9 //创建两个线程 10 Thread t1=new AccountThread(act); 11 Thread t2=new AccountThread(act); 12 13 //设置名字 14 t1.setName("t1"); 15 t2.setName("t2"); 16 17 //启动线程取款 18 t1.start(); 19 t2.start(); 20 21 } 22 23 }
运行结果:
可以看到,两个余额都是5000,这就说明出现了问题。但是要注意但是,出现问题是个概率事件,即这种情况可能发生,也可能不发生,关键在于当某个线程即将执行 this.setBalance(after);这行代码的时候,另一个线程是否已经执行withdraw()方法了,倘若另一个线程已经执行了withdraw()方法,那么即将执行this.setBalance(after) 这行代码的线程执行完这行代码后,肯定会出现问题。
为了放大这个问题,可以在this.setBalance(after)之前设置一个延时,这样一定出错,问题显示的也更加明显:
1 package thread_safe; 2 3 public class Account { 4 5 //账户 6 private String sctno; 7 8 //余额 9 private double balance; 10 11 public Account() { 12 13 } 14 15 public Account(String sctno, double balance) { 16 this.sctno = sctno; 17 this.balance = balance; 18 } 19 20 public String getSctno() { 21 return sctno; 22 } 23 24 public void setSctno(String sctno) { 25 this.sctno = sctno; 26 } 27 28 public double getBalance() { 29 return balance; 30 } 31 32 public void setBalance(double balance) { 33 this.balance = balance; 34 } 35 36 //取款方法 37 public void withdraw(double money){ 38 //t1和t2并发这个方法;t1、t2是两个栈。两个栈操作堆中同一个对象 39 //取款前的余额 40 double before=this.getBalance(); 41 double after=before-money;//取款后的余额 42 43 //进行1秒的睡眠,模拟网络延时 44 try { 45 Thread.sleep(1000); 46 } catch (InterruptedException e) { 47 // TODO Auto-generated catch block 48 e.printStackTrace(); 49 } 50 //更新余额 51 //若t1执行到这里,但还没来得及执行第44行代码,t2线程进来withdraw()方法了,此时一定出现问题。 52 this.setBalance(after); 53 54 } 55 56 }
这样之前所说的概率事件就成了一个肯定发生的事件,即每次运行都会出现问题。
解决方法:
1 public void withdraw(double money){ 2 3 //一下几行代码必须是线程排队的,不能并发 4 //一个线程将这里的代码全部执行完毕后,另一个代码才能进来 5 /* 6 * 线程同步机制的语法是: 7 * synchronized(){ 8 * 线程同步代码块 9 * 10 * } 11 * 12 * synchronized后面小括号中传的这个数据是相当重要的,这个数据必须是多线程共享的数据,才能达到多线程排队 13 * ()中写的是想让同步的线程,这里让t1,t2两个线程同步。 14 * 15 * 16 * 这里的共享对象是:账户对象 。账户对象是共享的,而这里的this就是账户对象 17 * 18 * 在java中,任何一个对象都有“一把锁”,其实这把锁就是一个标记(100个对象100把锁) 19 * 20 * 下面代码 的原理: 21 * 1.假设t1和t2线程并发,开始执行一下代码的时候,肯定有一个先一个后 22 * 2.假设t1先执行了,遇到synchronized,这个时候自动找后面“共享对象”(也就是这里的Account)的对象锁。找到之后并占有这把锁, 23 * 然后执行同步代码块中的程序,在程序执行过程中一直占有这把锁,直到同步代码块代码结束,这把锁才会释放。 24 * 3. 当t2想占有对象的锁的时候,发现t1已经占有,所以只能在同步代码块外等候t1执行完同步代码块里的程序,归还对象锁后,t2再占用对象的锁, 25 * 然后进入同步代码块执行程序 26 * 27 * 注:共享对象的选择一定要选好,这个对象一定要是需要排队执行的这些线程对象所共享的 28 */ 29 synchronized(this){ 30 double before=this.getBalance(); 31 double after=before-money;//取款后的余额 32 33 try { 34 Thread.sleep(1000); 35 } catch (InterruptedException e) { 36 37 e.printStackTrace(); 38 } 39 this.setBalance(after); 40 } 41 42 }
运行结果:
思考:
a. 在Account对象中再添加一个Obj对象(全局对象)
1 public class Account { 2 3 //账户 4 private String sctno; 5 6 //余额 7 private double balance; 8 9 10 //对象 11 Object obj=new Object(); //实例变量,(Account)对象是多线程共享的,所以Account对象中的实例变量obj也是被这些线程共享的
然后在synchronized()的括号内放入obj对象
1 synchronized(obj){ 2 double before=this.getBalance(); 3 double after=before-money;//取款后的余额 4 5 try { 6 Thread.sleep(1000); 7 } catch (InterruptedException e) { 8 9 e.printStackTrace(); 10 } 11 this.setBalance(after); 12 } 13 14 }
运行结果:
b.在withdraw()方法里添加一个对象(局部对象)。
1 package Thread_safe2; 2 3 public class Account { 4 5 //账户 6 private String sctno; 7 8 //余额 9 private double balance; 10 11 12 //全局对象 obj 13 Object obj=new Object(); //实例变量,(Account)对象是多线程共享的,所以Account对象中的实例变量obj也是被这些线程共享的 14 15 public Account() { 16 17 } 18 19 public Account(String sctno, double balance) { 20 this.sctno = sctno; 21 this.balance = balance; 22 } 23 24 public String getSctno() { 25 return sctno; 26 } 27 28 public void setSctno(String sctno) { 29 this.sctno = sctno; 30 } 31 32 public double getBalance() { 33 return balance; 34 } 35 36 public void setBalance(double balance) { 37 this.balance = balance; 38 } 39 40 //取款方法 41 public void withdraw(double money){ 42 //局部对象 obj2 43 Object obj2=new Object(); 44 synchronized(obj2){ 45 double before=this.getBalance(); 46 double after=before-money;//取款后的余额 47 48 try { 49 Thread.sleep(1000); 50 } catch (InterruptedException e) { 51 52 e.printStackTrace(); 53 } 54 this.setBalance(after); 55 } 56 57 } 58 59 }
运行结果:
分析原因 :obj2是局部变量,所以它不是共享对象
c.在withdraw()方法里添加字符串“abc”
synchronized("abc"){ double before=this.getBalance(); double after=before-money;//取款后的余额 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); }
这种方法也行,因为“abc”在字符串常量池中,但是需要注意的是,写“abc”是所有线程都同步,也就是说,如果这里再有几个线程t3、t4。。。。。。,它们也会同步,如果只需要t1和t2同步,其他线程不希望同步,那么就不能采用这样写法。如:
1 package Thread_safe2; 2 3 public class Test { 4 public static void main(String[] args){ 5 6 //创建一个账户对象 7 Account act=new Account("act-001",10000); 8 Account act1=new Account("act-002",10000000); 9 10 //创建两个线程 11 Thread t1=new AccountThread(act); 12 Thread t2=new AccountThread(act); 13 14 //额外创建一个线程 15 Thread t3=new AccountThread(act1); 16 17 //设置名字 18 t1.setName("t1"); 19 t2.setName("t2"); 20 21 t3.setName("t3"); 22 23 //启动线程取款 24 t1.start(); 25 t2.start(); 26 t3.start(); 27 28 } 29 30 }
(1)synchronized()中写this,这种情况下,只有t1与t2同步,也就是说,它们需要排队,后执行者等前面的归还对象锁(t1与t2共享的对象是act,所以它们两个要排队使用act的对象锁)后,才执行,而此时t3的对象是act1,括号中的this指的是当前对象,所以此时,t3与t1和t2都不同步。
(2)synchronized()中写“abc”,这种情况下,只有t1、t2、t3都同步,它们共同使用对象“abc”的锁,所以需要排队。
6.java中的三大变量
实例变量:存在堆中
静态变量:存在方法区中
局部变量:存在栈中
以上三大变量中,局部变量永远不会存在线程安全问题,因为局部变量在栈中,而一个线程一个栈,所以局部变量永远不会被共享所以也就不会存在安全问题了。
实例变量在堆中,静态变量在方法区中,而堆和方法区都只有一个,所以会堆和方法区都是多线程共享的,所以可能存在安全问题。
综上所述:局部变量和常量(因为常量不可修改)都没有线程安全问题,而成员变量(实例变量+静态变量)会存在线程安全问题
7. 如果使用局部变量的话,使用StringBuilder还是StringBuffer?
建议使用:StringBuilder。
虽然StringBuffer是线程安全的,StringBuilder是非线程安全的,然而将它们用作局部变量的时候,因为局部变量不存在线程安全问题,StringBuffer因为是线程安全的,反而效率会降低(因为StringBuffer的方法上都加synchronized了,每次运行到这里的时候都会去“锁池”里走一趟,所以会降低效率),所以这个时候应该使用StringBuilder。
非线程安全:ArrayList、HashMap、HashSet
线程安全:Vector、Hashtable
8. 总结
synchronized有两种写法
第一种:同步代码块。
synchronized(线程共享对象){
同步代码块
}
第二种:在实例方法上使用synchronized。表示共享对象一定是this,并且同步代码块 是整个方法体
第三种:在静态方法上,使用synchronized。表示使用类锁,类锁永远只有1把,就算创建了100个对象,那类锁也只有一把。
对象锁:一个对象一把锁,100个对象100把锁
类锁:100个对象,也可能只是一把锁
4个例子:
a.
1 package exam; 2 3 /* 4 * doOther()的执行是否需要等doSome()结束? 5 * 不需要,因为doOther()不需要占用this的锁,即当doSome()占用this锁执行的时候,不影响doOther()的执行 6 */ 7 public class Exam01 { 8 9 public static void main(String[] args) throws Exception{ 10 11 MyClass mc=new MyClass(); 12 Thread t1=new MyThread(mc); 13 Thread t2=new MyThread(mc); 14 15 t1.setName("t1"); 16 t2.setName("t2"); 17 18 t1.start(); 19 //睡眠的作用是保证t1先执行 20 Thread.sleep(1000); 21 t2.start(); 22 } 23 24 } 25 class MyThread extends Thread{ 26 MyClass mc=new MyClass(); 27 public MyThread(MyClass mc){ 28 this.mc =mc; 29 } 30 public void run(){ 31 if(Thread.currentThread().getName().equals("t1")){ 32 mc.doSome(); 33 } 34 if(Thread.currentThread().getName().equals("t2")){ 35 mc.doOther(); 36 } 37 } 38 } 39 class MyClass{ 40 public synchronized void doSome(){ 41 System.out.println("doSome begin"); 42 43 try { 44 Thread.sleep(1000*10); 45 } catch (InterruptedException e) { 46 // TODO Auto-generated catch block 47 e.printStackTrace(); 48 } 49 System.out.println("doSome over"); 50 } 51 52 public void doOther(){ 53 System.out.println("doOther begin"); 54 System.out.println("doOther over"); 55 } 56 }
b.
1 package exam; 2 3 4 /* 5 * doOther()的执行是否需要等doSome()结束? 6 * 需要,因为等doSome()执行结束了,才会释放this的锁,然后doOther()拿到this的锁了,才能继续执行 7 */ 8 public class Exam2 { 9 public static void main(String[] args) throws Exception{ 10 11 MyClass1 mc=new MyClass1(); 12 Thread t1=new MyThread1(mc); 13 Thread t2=new MyThread1(mc); 14 15 t1.setName("t1"); 16 t2.setName("t2"); 17 18 t1.start(); 19 //睡眠的作用是保证t1先执行 20 Thread.sleep(1000); 21 t2.start(); 22 } 23 24 } 25 class MyThread1 extends Thread{ 26 MyClass1 mc=new MyClass1(); 27 public MyThread1(MyClass1 mc){ 28 this.mc =mc; 29 } 30 public void run(){ 31 if(Thread.currentThread().getName().equals("t1")){ 32 mc.doSome(); 33 } 34 if(Thread.currentThread().getName().equals("t2")){ 35 mc.doOther(); 36 } 37 } 38 } 39 class MyClass1{ 40 public synchronized void doSome(){ 41 System.out.println("doSome begin"); 42 43 try { 44 Thread.sleep(1000*10); 45 } catch (InterruptedException e) { 46 // TODO Auto-generated catch block 47 e.printStackTrace(); 48 } 49 System.out.println("doSome over"); 50 } 51 52 public synchronized void doOther(){ 53 System.out.println("doOther begin"); 54 System.out.println("doOther over"); 55 } 56 }
c.
1 package exam; 2 3 /* 4 * doOther()的执行是否需要等doSome()结束? 5 * 不需要,因为MyClass对象是两个,两把锁 6 */ 7 8 public class Exam3 { 9 public static void main(String[] args) throws Exception{ 10 11 MyClass2 mc1=new MyClass2(); 12 MyClass2 mc2=new MyClass2(); 13 Thread t1=new MyThread2(mc1); 14 Thread t2=new MyThread2(mc2); 15 16 t1.setName("t1"); 17 t2.setName("t2"); 18 19 t1.start(); 20 //睡眠的作用是保证t1先执行 21 Thread.sleep(1000); 22 t2.start(); 23 } 24 25 } 26 class MyThread2 extends Thread{ 27 MyClass2 mc=new MyClass2(); 28 public MyThread2(MyClass2 mc){ 29 this.mc =mc; 30 } 31 public void run(){ 32 if(Thread.currentThread().getName().equals("t1")){ 33 mc.doSome(); 34 } 35 if(Thread.currentThread().getName().equals("t2")){ 36 mc.doOther(); 37 } 38 } 39 } 40 class MyClass2{ 41 public synchronized void doSome(){ 42 System.out.println("doSome begin"); 43 44 try { 45 Thread.sleep(1000*10); 46 } catch (InterruptedException e) { 47 // TODO Auto-generated catch block 48 e.printStackTrace(); 49 } 50 System.out.println("doSome over"); 51 } 52 53 public synchronized void doOther(){ 54 System.out.println("doOther begin"); 55 System.out.println("doOther over"); 56 } 57 58 }
d.
1 package exam; 2 /* 3 * doOther()的执行是否需要等doSome()结束? 4 * 需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把 5 */ 6 public class Exam4 { 7 public static void main(String[] args) throws Exception{ 8 9 MyClass3 mc1=new MyClass3(); 10 MyClass3 mc2=new MyClass3(); 11 Thread t1=new MyThread3(mc1); 12 Thread t2=new MyThread3(mc2); 13 14 t1.setName("t1"); 15 t2.setName("t2"); 16 17 t1.start(); 18 //睡眠的作用是保证t1先执行 19 Thread.sleep(1000); 20 t2.start(); 21 } 22 23 } 24 class MyThread3 extends Thread{ 25 MyClass3 mc=new MyClass3(); 26 public MyThread3(MyClass3 mc){ 27 this.mc =mc; 28 } 29 public void run(){ 30 if(Thread.currentThread().getName().equals("t1")){ 31 mc.doSome(); 32 } 33 if(Thread.currentThread().getName().equals("t2")){ 34 mc.doOther(); 35 } 36 } 37 } 38 class MyClass3{ 39 // synchronized 出现在静态方法上找的是类锁 40 public synchronized static void doSome(){ 41 System.out.println("doSome begin"); 42 43 try { 44 Thread.sleep(1000*10); 45 } catch (InterruptedException e) { 46 // TODO Auto-generated catch block 47 e.printStackTrace(); 48 } 49 System.out.println("doSome over"); 50 } 51 52 public synchronized static void doOther(){ 53 System.out.println("doOther begin"); 54 System.out.println("doOther over"); 55 } 56 57 58 59 }
9.开发中如何解决线程安全问题?
虽然使用synchronized()会解决线程安全问题,保证线程的同步,但是这种方法也有缺点:会降低用户的吞吐量(并发量),使得程序执行效率降低,所以尽量避免使用这种方法。
方法一:尽量使用局部变量代替实例变量和静态变量。
方法二:如果必须使用实例变量,可以考虑创建多个对象,这样就可以避免共享实例变量的内存。(1个线程对应1个对象,100个线程对应100个对象,若线程之间不存在共享对象,也就不会出现安全问题)。
方法三:如果不能使用局部变量,对象也不能创建多个,这个时候就只能使用synohronized.
10. 线程部分的其他内容
10.1 守护线程
java语言中,线程分为两类,一类叫用户线程,另一类叫守护线程(后台线程)。
守护线程的特点:一般是个死循环;只要用户线程结束,守护线程就会自动结束。垃圾回收线程就是一个守护线程,main方法就是一个用户线程。
守护线程用在什么地方?
10.2 定时器
10.3 实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)
10.4 关于Object类中的wait和notify方法。(生产者和消费者模式)