1、线程同步问题
当多个线程同时操作同一个数据时,就会产生线程同步问题。
为了确保在任何时间点一个共享的资源只被一个线程使用,使用了“同步”。当一个线程运行到需要同步的语句后,CPU不去执行其他线程中的、可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能去执行其他线程中的相关代码块,这就是线程同步。
下面这个例子中,多个线程共同操作同一个账户里的余额,就有可能出现线程同步错误。
//模拟一个账户,其中有余额1000元。取钱时如果不足1000元就不能取。
public class Account {
private int balance = 1000;
public void qu(){
if(balance>=1000){
try {
// 模拟CPU时间片到期,导致的线程切换
Thread.sleep(100); //
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= 1000;
System.out.println("取了1000元,balance = " + balance);
}
}
}
class ThreadTest extends Thread{
private Account account;
public ThreadTest(Account account){
this.account = account;
}
public void run(){
account.qu(); //在线程类中调用取钱的方法
}
}
//模拟多个人同时操作同一个账户。结果有可能出现这样一种情况:
//账户中共有余额1000元,但却被取走了2000元。
public static void main(String[] args) {
Account account = new Account();
ThreadTest tt1 = new ThreadTest(account); //给每个线程传入的是同一个账户
ThreadTest tt2 = new ThreadTest(account); //给每个线程传入的是同一个账户
tt1.start();
tt2.start();
}
运行结果:
取了1000元,balance = -1000
取了1000元,balance = -1000
2、线程同步解决方案一synchronized
解决办法:加上synchronized关键词,使取钱的方法成为一个同步方法。
一旦一个包含锁定方法(用synchronized修饰)的线程被CPU调用,其他线程就无法调用相同对象的锁定方法。当一个线程在一个锁定方法内部,所有试图调用该方法的同实例的其他线程必须等待
synchronized:同步锁(互斥锁)
1.在java语言中,引入了同步锁的概念,每个对象都有一个与之关联的内部锁(排他锁),用以保证共享数据的安全性问题。
2.关键词synchronized用来给某个方法或某段代码加上一个同步锁。
3.当调用者调用此方法时,必须获得这把锁才可以调用。
4.当某个调用者获得这把锁之后,其他调用者就无法获得了。
5.当调用结束后,调用者释放这把锁,此时其他调用者才可以获得。
6.这个机制保障了某个同步方法同时只能有一个调用者。
1.锁定方法
public class Account {
private int balance = 1000;
public synchronized void qu(){ // 同步方法
if(balance>=1000){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= 1000;
System.out.println("取了1000元,balance = " + balance);
}
}
}
2.锁定代码块
public class Account {
private int balance = 1000;
public void qu(){
System.out.println("Account.qu");
synchronized(this){ // 同步块
if(balance>=1000){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= 1000;
System.out.println("取了1000元,balance = " + balance);
}
}
}
}
3、死锁问题
同步锁具有互斥作用,即排他性。但是这又造成了另外一个问题:死锁。
为了完成一个功能,需要调用两个资源。但是,当两个线程同时调用这两
个资源时,就会出现这样的现象:两个线程都不放弃抢到的一个资源,而另一个资源却永远也抢不到。
例:
public class TestLock {
public static String objA = "strA";
public static String objB = "strB";
public static void main(String[] args) {
Lock1 l1=new Lock1();
Thread t1=new Thread(l1);
Lock2 l2=new Lock2();
Thread t2=new Thread(l2);
t1.start();
t2.start();
}
}
public class Lock1 implements Runnable{
@Override
public void run() {
try{
System.out.println("Lock1 running");
while(true){
synchronized(TestLock.objA){
System.out.println("Lock1 lock strA");
Thread.sleep(5000);//获取strA后先等一会儿,让Lock2有足够的时间锁住strB
synchronized(TestLock.objB){
System.out.println("Lock1 lock strB");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
package com.neuedu.thread.lock;
public class Lock2 implements Runnable{
@Override
public void run() {
try{
System.out.println("Lock2 running");
while(true){
synchronized(TestLock.objB){
System.out.println("Lock1 lock strB");
Thread.sleep(5000);//获取strA后先等一会儿,让Lock2有足够的时间锁住strB
synchronized(TestLock.objA){
System.out.println("Lock1 lock strA");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
1.产生死锁的必要条件:
虽然线程在运行过程中可能会发生死锁,但产生死锁是必须具备一定条件的。产生死锁必须同时具备下面四个必要条件,只要其中任意一个条件不成立,死锁就不会产生:
- 互斥条件:线程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个线程占用。如果此时还有其他进程请求该资源,则请求进程只能等待,直至占有该资源的线程释放该资源。
- 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己以获得的资源保持不放。
- 不可抢占条件:线程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时由自己释放。
- 循环等待条件。在发生死锁时,必然存在一个线程—资源的循环链,即线程集合{P0,P1,P2,P3,...,Pn}中的P0正在等待P1占用的资源,P1正在等待P2占用的资源,... ... ,Pn正在等待已被P0占用的资源。
2.处理死锁的方法
- 预防死锁。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个来预防产生死锁。
- 避免死锁。在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而可以避免产生死锁。
- 检测死锁。通过检测机构及时地检测出死锁的发生,然后采取适当的措施,把进程从死锁中解脱出来。
- 解除死锁。当检测到系统中已发生死锁时,就采取相应的措施,将进程从死锁状态中解脱出来。常用方法是---撤销一些进程,回收他们的资源,将他们分配给已处于阻塞状态的进程,使其能继续运行。
4、 线程同步的第二种解决方案 wait() notify()
1.生产者消费者问题
生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个线程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。
package test06;
//仓库(有界缓冲区)
class Storage{
//库存
private int count = 0;
//供生产者调用的生产的方法
public synchronized void set(){
if(count>=5){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("生产了一个,仓库中有:" + count);
this.notify();
}
//供消费者调用的消费的方法
public synchronized void get(){
if(count<=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println("消费了一个,仓库中有:" + count);
this.notify();
}
}
//生产者
class Producer extends Thread{
private Storage storage;
public Producer(Storage storage){
this.storage = storage;
}
public void run(){
for(int i=0;i<50;i++){
this.storage.set();
}
}
}
//消费者
class Customer extends Thread{
private Storage storage;
public Customer(Storage storage){
this.storage = storage;
}
public void run(){
for(int i=0;i<50;i++){
this.storage.get();
}
}
}
public class Test2 {
public static void main(String[] args) {
Storage storage = new Storage();
Producer producer = new Producer(storage);
Customer customer = new Customer(storage);
customer.start();
producer.start();
}
}
总结线程同步的常用方法有以下两种:
1、synchronized
2、wait与notify
5、 守护线程(了解)
Java的线程分为两种:User Thread(用户线程)、DaemonThread(守护线程)。用个比较通俗的比喻,任何一个守护线程都是整个JVM中所有非守护线程的保姆。只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束是,守护线程随着JVM一同结束工作,Daemon作用是为其他线程提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),他就是一个很称职的守护者。
thread.setDaemon(true) //设置线程为守护线程,该设置必须在thread.start()之前设置
thread.isDaemon() //可以判断当前线程是否为守护线程