1.为什么是重点?
以后在开发中,项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动等,都已经实现完了。这些代码都不需要编写,最重要的是要知道:编写的程序需要放到一个多线程的环境下运行,更需要关注这些数据在多线程并发的环境下是否是安全的。
2.什么时候数据在多线程并发的环境下会存在安全问题?
三个条件:
(1)条件1:多线程并发
(2)条件2:多线程有共享的数据
(3)条件3:共享的数据有修改的行为
满足以上3个条件之后,就会存在线程安全问题。
3.如何解决线程安全问题?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题。
此时应当使线程排队执行(不能并发)。用排队执行解决线程安全问题,这种机制称为:线程同步机制。(这是专业术语的叫法,实际上就是线程不能并发了,必须排队执行)。线程同步(也就是线程排队)会牺牲一部分效率,但是数据安全是第一位的。
4.线程同步涉及到的两个专业术语
4.1 异步编程模型
线程t1和t2各自执行各自的,相互不管,谁也不需要等谁。(其实就是多线程并发,效率较高)
4.2 同步编程模型
线程t1和t2,在某一线程执行的时候,另一个线程必须等待正在执行的线程,一直到其执行完毕为止。两个线程之间发生了等待关系,线程排队执行,效率较低。
5.例子分析
1 packagethread_safe;2
3 public classAccount {4
5 //账户
6 privateString sctno;7
8 //余额
9 private doublebalance;10
11 publicAccount() {12
13 }14
15 public Account(String sctno, doublebalance) {16 this.sctno =sctno;17 this.balance =balance;18 }19
20 publicString getSctno() {21 returnsctno;22 }23
24 public voidsetSctno(String sctno) {25 this.sctno =sctno;26 }27
28 public doublegetBalance() {29 returnbalance;30 }31
32 public void setBalance(doublebalance) {33 this.balance =balance;34 }35
36 //取款方法
37 public void withdraw(doublemoney){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 packagethread_safe;2
3 public class AccountThread extendsThread {4
5 //两个线程必须共享同一个账户对象
6 privateAccount act;7
8 //通过构造方法传递过来账户对象
9 publicAccountThread(Account act){10 this.act=act;11 }12 public voidrun(){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 packagethread_safe;2
3 public classTest {4 public static voidmain(String[] args){5
6 //创建一个账户对象
7 Account act=new Account("act-001",10000);8
9 //创建两个线程
10 Thread t1=newAccountThread(act);11 Thread t2=newAccountThread(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 packagethread_safe;2
3 public classAccount {4
5 //账户
6 privateString sctno;7
8 //余额
9 private doublebalance;10
11 publicAccount() {12
13 }14
15 public Account(String sctno, doublebalance) {16 this.sctno =sctno;17 this.balance =balance;18 }19
20 publicString getSctno() {21 returnsctno;22 }23
24 public voidsetSctno(String sctno) {25 this.sctno =sctno;26 }27
28 public doublegetBalance() {29 returnbalance;30 }31
32 public void setBalance(doublebalance) {33 this.balance =balance;34 }35
36 //取款方法
37 public void withdraw(doublemoney){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(doublemoney){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 classAccount {2
3 //账户
4 privateString sctno;5
6 //余额
7 private doublebalance;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 packageThread_safe2;2
3 public classAccount {4
5 //账户
6 privateString sctno;7
8 //余额
9 private doublebalance;10
11
12 //全局对象 obj
13 Object obj=new Object(); //实例变量,(Account)对象是多线程共享的,所以Account对象中的实例变量obj也是被这些线程共享的
14
15 publicAccount() {16
17 }18
19 public Account(String sctno, doublebalance) {20 this.sctno =sctno;21 this.balance =balance;22 }23
24 publicString getSctno() {25 returnsctno;26 }27
28 public voidsetSctno(String sctno) {29 this.sctno =sctno;30 }31
32 public doublegetBalance() {33 returnbalance;34 }35
36 public void setBalance(doublebalance) {37 this.balance =balance;38 }39
40 //取款方法
41 public void withdraw(doublemoney){42 //局部对象 obj2
43 Object obj2=newObject();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 packageThread_safe2;2
3 public classTest {4 public static voidmain(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=newAccountThread(act);12 Thread t2=newAccountThread(act);13
14 //额外创建一个线程
15 Thread t3=newAccountThread(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 packageexam;2
3 /*
4 * doOther()的执行是否需要等doSome()结束?5 * 不需要,因为doOther()不需要占用this的锁,即当doSome()占用this锁执行的时候,不影响doOther()的执行6 */
7 public classExam01 {8
9 public static void main(String[] args) throwsException{10
11 MyClass mc=newMyClass();12 Thread t1=newMyThread(mc);13 Thread t2=newMyThread(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 extendsThread{26 MyClass mc=newMyClass();27 publicMyThread(MyClass mc){28 this.mc =mc;29 }30 public voidrun(){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 classMyClass{40 public synchronized voiddoSome(){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 voiddoOther(){53 System.out.println("doOther begin");54 System.out.println("doOther over");55 }56 }
b.
1 packageexam;2
3
4 /*
5 * doOther()的执行是否需要等doSome()结束?6 * 需要,因为等doSome()执行结束了,才会释放this的锁,然后doOther()拿到this的锁了,才能继续执行7 */
8 public classExam2 {9 public static void main(String[] args) throwsException{10
11 MyClass1 mc=newMyClass1();12 Thread t1=newMyThread1(mc);13 Thread t2=newMyThread1(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 extendsThread{26 MyClass1 mc=newMyClass1();27 publicMyThread1(MyClass1 mc){28 this.mc =mc;29 }30 public voidrun(){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 classMyClass1{40 public synchronized voiddoSome(){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 voiddoOther(){53 System.out.println("doOther begin");54 System.out.println("doOther over");55 }56 }
c.
1 packageexam;2
3 /*
4 * doOther()的执行是否需要等doSome()结束?5 * 不需要,因为MyClass对象是两个,两把锁6 */
7
8 public classExam3 {9 public static void main(String[] args) throwsException{10
11 MyClass2 mc1=newMyClass2();12 MyClass2 mc2=newMyClass2();13 Thread t1=newMyThread2(mc1);14 Thread t2=newMyThread2(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 extendsThread{27 MyClass2 mc=newMyClass2();28 publicMyThread2(MyClass2 mc){29 this.mc =mc;30 }31 public voidrun(){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 classMyClass2{41 public synchronized voiddoSome(){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 voiddoOther(){54 System.out.println("doOther begin");55 System.out.println("doOther over");56 }57
58 }
d.
1 packageexam;2 /*
3 * doOther()的执行是否需要等doSome()结束?4 * 需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把5 */
6 public classExam4 {7 public static void main(String[] args) throwsException{8
9 MyClass3 mc1=newMyClass3();10 MyClass3 mc2=newMyClass3();11 Thread t1=newMyThread3(mc1);12 Thread t2=newMyThread3(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 extendsThread{25 MyClass3 mc=newMyClass3();26 publicMyThread3(MyClass3 mc){27 this.mc =mc;28 }29 public voidrun(){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 classMyClass3{39 //synchronized 出现在静态方法上找的是类锁
40 public synchronized static voiddoSome(){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 voiddoOther(){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方法。(生产者和消费者模式)