Java线程学习

线程

一、线程是什么

1、程序、进程和线程的区别

  • 程序 :一段静态代码,是应用程序执行的蓝本。
  • 进程 :程序的一次动态执行过程,对应了从代码加载、执行到执行完毕的一个完整过程。
  • 线程 :是进程内部单一的一个顺序控制流。一个进程在执行过程中,可以产生多个线程,每个线程有自己的生命周期。线程相当于进程中的一个个线索。

进程和线程的关系

一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源。但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。(线程私有)

为什么要使用多线程

  • 从计算机底层:线程可以比作是轻量级的进程,是程序执行的最小单元,线程间的切换和调度的成本远远小于进程。另外,多核CPU时代,意味着多个线程可以同时运行,这减少了线程上下文切换的开销
  • 从当代互联网发展趋势来说:现在系统动辄要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
  • 以前单核时代:多线程主要是为了提高单进程利用CPU和IO系统的效率。假设只允许了一个Java进程的情况,当我们请求IO的时候,如果Java进程中只有一个线程,此线程被IO阻塞则整个进程阻塞
  • 现在的多核时代:多核时代主要为了提高进程利用多核CPU的能力。

使用多线程可能带来问题

并发编程的目的是为了能提高程序的执行效率提高程序的运行速度,但是并发编程并不总是能提高程序运行速度,而且会遇到很多问题:内存泄露、死锁、线程不安全。

2、线程的状态和生命周期

  • 线程的五种状态

新建、就绪、运行、阻塞、死亡

(阻塞状态只能到就绪状态,而不能直接到运行)

  • 每个Java程序都有一个主线程,要想实现多线程,就要在主线程中创建新的线程对象。

Java使用Thread类及其子类表示线程,新建的线程在它的一个完整生命周期中通常要经历五种状态。

3、为什么程序计数器是私有的?

  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪。因此,线程的程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

4、并发和并行

  • 并发:两个及两个以上的作业在同一时间段内执行
  • 并行:两个及两个以上的作业在同一时刻执行 (同时)

5、线程安全和线程不安全

  • 线程安全:多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性
  • 线程不安全:多线程环境下,对于同一份数据,多个线程同时访问会导致数据混乱、错误或者丢失。

二、理解线程的五种状态

1、示例代码



public class Mathine extends Thread{
 
	public void run(){
 	   
	   for ( int a=0;a<10;a++){
	     
			System.out.println(currentThread().getName()+":"+a);
	      try {
	    	  //线程睡眠,进入阻塞状态,主动放弃处理机
	           Thread.sleep(100);
	          }catch (InterruptedException e) {throw new RuntimeException(e);
	          }
	        }
       }
 
     public static void main(String[] args) {
    	 //线程的新建。和普通对象一样,在栈里占一块空间
	       Mathine mathine1 = new Mathine();
	       Mathine mathine2 = new Mathine();
	       
	       mathine1.setName("m1");//设置线程的名称
	       mathine2.setName("m2");
		   mathine1.setPriority(Thread.MAX_PRIORITY);//设置线程的优先级
           mathine2.setPriority(Thread.MIN_PRIORITY);
		   System.out.println("Priority of m1:"+mathine1.getPriority());
           System.out.println("Priority of m2:"+mathine2.getPriority());
           
           //线程处于就绪状态,可以有多个。就绪需要资源:指令计数器和栈空间。随机被处理机调用
           mathine1.start();
           mathine2.start();
           //通过mathine1对象调用run方法
           mathine1.run();
           
  }
 }

Mathine mathine1 = new Mathine();

Mathine mathine2 = new Mathine();

创建Methine类的对象—mathine1,mathine2,这两个对象是线程对象,对于线程来说,此时是两个线程处于新建状态。

mathine1.start();

mathine2.start();

使线程1和线程2处于就绪状态,一旦处于就绪状态,就要分配资源。虚拟机需要为这两个线程分配资源:

  • 为线程开辟栈空间;mathine1、mathine2
  • 为线程分配指令计数器PC;占领处理机后执行代码指令

此时两个线程处于就绪状态,根据处理机的调度,随机占领处理机,mathine1线程占领处理机,就会执行线程对象入口方法也就是的run方法,在mathine1线程run方法栈帧中开辟空间

currentThread()

显示当前线程的地址

Thread.sleep(100);

线程睡眠,进入阻塞状态,主动放弃处理机。不会释放资源,资源保存在寄存器中。

mathine1.run();

实例方法通过对象调用,主线程通过对象调用run方法。在主线程的run方法栈帧里开辟空间。

主线程主方法执行完不一定程序结束,因为可能还有其他线程在执行方法。

程序结束,线程进入死亡状态:释放所有资源。

2、可以直接调用Thread类的run方法吗

当new一个Thread时,线程进入新建状态,调用start()方法,线程会进入就绪状态,然后等待处理机分配时间片,当分配到时间片就可以运行,线程会自动调用自己的run方法,这才是多线程工作;

如果直接调用run()方法,会把run()方法当做main线程下的一个普通方法去执行,并不会在某个线程中执行,这不是多线程工作。

三、关于Java中线程调度

1、线程命名、设置优先级

给线程设定属性只能在创建状态

 		   mathine1.setName("m1");//设置线程的名称
	       mathine2.setName("m2");
		   mathine1.setPriority(Thread.MAX_PRIORITY);//设置线程的优先级
            mathine2.setPriority(Thread.MIN_PRIORITY);

理论上,Java上层根据线程优先级调度 ,优先级高的先执行,同一优先级则随机调度。但是实际情况中,因为Java线程调度还要依赖于底层 ,所以不同系统甚至不同次运行得到的结果都有可能不相同,Java中的线程调度是非常混乱的,所以设置线程的优先级不代表优先级高的一定被优先执行。

2、Java线程调度策略

实例代码



/**
*演示线程调度策略为时间片和轮转--JavaDoc
*演示设置后台线程的方法和运行特点
*演示暂停当前线程的方法yield()
@author admin
*@version 1.0  
*/
public class MultiThread1
{
	public static void main(String[] args)
	{
		MyThread mt=new MyThread();
		 //设定mt线程为后台线程,理论上前台线程结束后台线程随即终止,
		 mt.setDaemon(true);
		 mt.start();
		int index=0;
		while(true)
		{
			 if(index++==10)break; 
			System.out.println("main:"+Thread.currentThread().getName()+" "+index);
		}
	}
}

class MyThread extends Thread
{
	 public void run()
	{
		while(true)
		{System.out.println(getName());
			yield();//暂停当前线程。从运行状态到就绪状态,让主线程执行。
		     
		}
	} 
}

理论上前台线程结束后台线程随即终止 。那么,主线程运行while循环里的代码,一直到idnex==10,退出循环,线程结束,后台线程也会随机终止,那么后台线程理论上完全不能运行,但实际情况是这样吗?并不是,得到的结果是,后台线程也有运行。说明java线程调度策略为时间片轮转 ,并不会一直让某一个线程运行,会分出时间片给后台程序,并且时间片并不是等时分配。

后台线程时间片多,执行yield(),让后台线程从运行状态到就绪状态,让步给主线程执行。但就绪状态依然随时会占用处理机,意义不大。

4、内部类对象对外部类的实例变量的争用

实例代码

package test;

/**
*演示利用内部类实现多线程对同一个实例变量的访问  
*/
public class MultiThread3
{
	public static void main(String[] args)
	{
		//创建一个外部类对象
		MyThread mt=new MyThread();
		//创建新线程,调用start()方法
		//创建四个外部类对象的内部类对象,四个线程对象都执行自己的run方法,四个内部类的线程对象使用同一个index
		//四个线程对象争用同一个index实例变量,每次看到的index都不一样,可能一个线程看到的是1,另一个线程看到的9
		mt.getThread().start();
		mt.getThread().start();
		mt.getThread().start();
		mt.getThread().start();
		 
	}
}

class MyThread  
{
	int index=0;
	/***************************************/
	//私有的内部类,只能在外部类的方法里才能创建内部类对象
	private class InnerThread extends Thread
	{
		public void run()
		{
			//在外部类中找index,不能写成this.index,这代表在内部类中找
			while(index<=10)
			{   
				System.out.println(Thread.currentThread().getName()+":"+index);
			    index=index+1;
			}
		}
	}
	/***************************************/
	Thread getThread()
	{
		//this所指外部类对象的内部类对象,创建内部类对象
		//内部类对象拥有所指外部类对象的引用
		return this.new InnerThread();
	}
	 
}

MyThread类里有一个内部类InnerThread,是私有内部类,私有内部类的对象只能在外部类的方法里才能创建,只有外部类的方法才能识别。

外部方法getThread(),

this.new InnerThread()

创建this所指外部类对象的内部类对象。new InnerThread()是创建一个内部类对象,而这个内部类对象是线程对象,此时就是线程的新建状态 。内部类对象拥有this所指外部类对象的全部引用。

mt.getThread().start();

通过外部类对象调用外部方法来创建一个线程对象,并调用start方法,线程进入就绪状态,有指令计数器和开辟了栈空间。

线程处于就绪状态,占领处理机的线程(即运行)会执行自己线程的内部类对象的run方法,index+1,时间片到了,就会把处理机让步下一个线程。理论上,index应该从0一直加到10,但是运行结果却并不是这样,这是因为线程的争用资源 。解释如下:

创建了四个线程对象,四个线程对象访问的是同一个index变量,这个时候就出现线程对实例变量争用。例如当第一个线程运行,执行内部类的run方法,找外部类的index变量,看到index是0,此时时间片到了(还没来得及执行index++的操作),下一个线程运行,执行内部类的run方法,同样找这个相同的index变量,看到的还是0,…以此类推,而当再次轮到第一个线程执行,此时index可能已经加到了5,但是第一个线程还在执行上一次的方法,认为index是0,于是index+1,线程所看到的值和实际的值不一致,最后就导致index并不是依次增加,很混乱 。----线程不安全的

解决方案:加锁。让线程看到的index值和实际值要一样。在线程的方法执行完前加锁,即便时间片到了也不解锁,一直到执行完。

四、创建线程的方式

1、方式一就是前面代码那种通过一个类继承Thread类,创建这个类的对象就是线程对象。

2、方式二,实现Runnable接口


public class ThreadTest1 { 
	 public static void main(String args[]) {
		 
		 	//创建一个实现了Runnable的类的对象mt
		 	MyThread1 mt = new MyThread1(); //产生一个Runnable对象mt
		
			//创建线程对象,线程新建
		   Thread t = new Thread(mt); //以Runnable对象构造一个Thread类的实例
		   //启动线程,线程进入就绪状态
		   t.start();
		   //执行到里,主方法结束,主线程结束,程序还没结束,处理机给线程t执行,线程t执行MyThread对象的run方法
		   //t线程占有处理机,执行mt所指对象的run方法
	  } 
 } 

class MyThread1 implements Runnable { 
	 int i=980; 
	 public void run() { 
	  while (true) { //this所指对象mt的i,也就是MyThread1里的i
	   System.out.println("Hello " + this.i++); 
	   if (this.i == 1000) break; 
	  } 
	 } 
}


  • MyThread类实现Runnable接口,但此时并不是一个线程类 。线程类必须要有start()方法和run()方法,而MyThread类中只有run()。所以,MyThread1 mt = new MyThread1(); 这里只是创建了一个MyThread对象,并没有创建线程
  • **Thread t = new Thread(mt); ** 创建线程对象
  • t.start(); ,线程进入就绪状态。当时间片给到它,就会占领处理机,执行MyThread对象的run()方法

五、终止线程的方式

1、代码如下


class ThreadDemo implements Runnable{
    private boolean flag = true;
    
    public void stopRunning(){
        this.flag = false;
    }
    public void run(){//this所指对象tt的flag
        System.out.println("falg = " + this.flag);
        while(this.flag){
            try{
                Thread.currentThread().sleep(1000);
            }
            catch(InterruptedException e){
                System.out.println("线程被终止");
             return;
			}

            System.out.println("I love you");
        }
    }
}
public class ThreadTest{
	public static void main(String[] args){
        ThreadDemo tt = new ThreadDemo();//创建对象,在堆里
        Thread t = new Thread(tt);//创建线程对象
        t.start();
        try{
            Thread.currentThread().sleep(5000);
                 boolean b=t.isAlive();
			     System.out.println("t isAlive(): "+ b);

			     //t.stop();
			     t.interrupt();
			     //自然终止
//			     tt.stopRunning();
        }
        catch(InterruptedException e){
            System.out.println(e);
        }
    }
}

  • tt.stopRunning(); 调用方法,自然终止

输出结果:

falg = true
I love you
I love you
I love you
I love you
t isAlive(): true
I love you

运行过程梳理:

运行程序,主线程主方法执行, t.start(); 线程对象t进入就绪状态。主线程执行到 Thread.currentThread().sleep(5000); 主线程休息五秒,放弃处理机,线程t开始占领处理机,执行tt所指对象的run方法 ,输出flag=true , 然后执行Thread.currentThread().sleep(1000); 睡一秒,醒来后,输出 I love you ,此时主线程还在睡,所以t线程继续执行,继续循环,又睡一秒,醒来后,输出i love you,一直这样循环,当输出到第四个i love you,线程t第五次睡眠,此时主线程醒了,于是主线程继续执行,输出t isAlive(): true ,然后执行tt.stopRunning(); 调用tt所指对象的stopRunning方法,此时flag变为false。执行完后,主方法结束主线程死亡,接着线程t醒来继续执行(哪里睡着哪里醒来),输出i love you ,此时循环条件已经不满足,于是退出循环,线程结束。

  • t.interrupt(); 通过制造异常让线程死亡

这个方法的意思是,在主线程执行到这里,强行唤醒还在睡眠的t线程,使t线程进入就绪状态,给出中断线程的信号,t线程出现异常,捕获到中断异常,执行输出**“线程终止”**

  • t.stop()

最粗暴,主线程执行到这里,发现t线程还在存活,直接将睡眠的t线程终止,所以之后t线程没有任何输出。stop()可能会造成数据混乱,不建议使用

六、锁

6.1、synchronized

1、什么情况需要加锁?

一个线程执行某个代码块时不能中断,且只能由一个线程执行完成后才能下一个线程。比如购票系统、上厕所等原子性的操作,不能被其他线程中断,这时就要对原子性的操作进行加锁。但是加锁后并发性会降低。

2、

代码:购票例子

每一个对象都有一个监视器,或者叫做锁。保证原子操作不能被中断

/**
*演示多线程中的同步块和同步方法
*线程调度策略为时间片和轮转
*/
		 public class TicketsSystem1
		 {
			public static void main(String[] args)
			{
              
              	//创建对象
				SellThread st=new SellThread();
						 
				 new Thread(st).start();
				 new Thread(st).start();
			     new Thread(st).start();
			}
		}
		class SellThread implements Runnable
		{   int tickets=100;
			Object obj=new Object();
			public void run()
			{  //int a=0;
				while (this.tickets>0)
			   {
				try
				  {
					 Thread.sleep(10);//第一个线程出来后锁会打开,此时让这个线程先休息一会,不然其他在锁池的线程可能没机会购买
					}
				  catch(Exception e)
				  {
					e.printStackTrace();
				   } 	
				/*每一个对象都有一个监视器,或者叫做锁。保证原子操作不能被中断
				  把obj对象的锁加锁实现语句块同步*/
					synchronized(obj) //判断这个对象是否开锁,线程执行到这里,如果锁是关闭的就进入锁池等待(线程为阻塞状态),如果锁是打开的,就执行线程,并关闭锁
					 {	//对语句块加锁,叫做块锁。要想执行下面的语句块(买票),必须当前锁是打开的,如果锁是关闭就进入锁池等待	    
					  try
					  {
						 Thread.sleep(10);//进入代码块的线程,这里即便睡眠,锁依然是关闭的,其他线程也进不来
						}
					  catch(Exception e)
					  {
						e.printStackTrace();
					   } 
								 
					   if(tickets>0)
						{//this所指st对象的tickets
						 System.out.println("obj:"+Thread.currentThread().getName()+
												" sell tickets:"+this.tickets);
						this.tickets--;
						 System.out.println("obj:"+Thread.currentThread().getName()+
												" sell tickets after:"+this.tickets);
							}
								 
						   } 
			 }
		 } 
	  }
	

**synchronized(obj) **

给OBJ对象加锁,以实现代码块同步。当线程执行到这里,如果锁是打开的,则执行下面的代码块,并且将锁关闭;其他线程来到这里,面对关闭的锁,只能进入锁池等待。

Thread.sleep(10);

执行代码块的线程,即便这里要睡眠,但此时锁依然是关闭的,只要线程还在执行,其他线程就不能进来打断它。这就是加锁的意义

Thread.sleep(10);

执行完买票代码后,锁就会开启。此时执行完买票操作的线程,还在运行,于是继续执行循环,到这里,系统让它睡眠,目的是为了给其他还在锁池等待的线程机会购票。否则刚买完票的线程可能又会被分配去买票。强制让买完票的线程放弃处理机。

3、锁的分类

  • 代码块锁:给对象加锁,实现代码块同步。锁指定对象
  • 方法锁:本质也是对象锁。对调用该方法的对象加锁,一旦一个线程占用这个方法,调用该方法的对象的其他方法也无法被其他线程占用。锁当前对象实例
  • 类锁:每个class也有一个锁,是这个class所对应的反射对象的锁。锁的范围更大。静态方法采用类锁来同步。锁当前类,会给类的所有对象实例加锁

4、方法锁实现同步

卖票代码:

package com.xqh.in;

/**方法锁实现同步*/
class ThreadDemo implements Runnable{
    private int tickets = 20;
	public  void run(){
       while(this.tickets != 0){//tickets==0,票卖完了,退出循环
		   try{
               Thread.currentThread().sleep(10);//上一次买票的线程买完票后,锁打开,为防止它再次买票,强制让他睡眠,让出处理机,给其他在锁池的线程机会
             }
             catch(InterruptedException e){
               System.out.println(e);
            } 
		 
		   //调用this所指的ThreadDemo对象的sale方法
           this.sale();
       }

    }
	//私有方法只能在同一个类的其他方法里调用
    private synchronized void sale(){//先看调用sale方法的对象(this所指对象)锁是否打开,如果是打开的,执行下面的代码,并把锁关锁;如果锁是关闭的,就进入锁池等待
        if(this.tickets > 0){
             System.out.println(Thread.currentThread().getName() + " : tickets = " + this.tickets);
              try{
                Thread.currentThread().sleep(100);
            }
            catch(InterruptedException e){
                System.out.println(e);
           }
			  this.tickets--;
             System.out.println(Thread.currentThread().getName() + " : tickets after= " + this.tickets );
       }
    }
}


public class ThreadTest2{
    public static void main(String[] args){
    	//创建实现了runnable接口的类的对象
        ThreadDemo tt = new ThreadDemo();
        //创建线程,并命名
        Thread t1 = new Thread(tt, "t1");
        Thread t2 = new Thread(tt, "t2");
        Thread t3 = new Thread(tt, "t3");
        Thread t4 = new Thread(tt, "t4");
        //启动线程,线程进入就绪状态。得到时间片后就会占领处理机,执行tt所指对象的run方法
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        //执行到最后,主线程死亡
    }
}

private synchronized void sale()

对方法加锁,而且是私有方法。线程执行到这里,先判断调用这个方法的对象(this所指对象)的锁是否打开,如果打开就继续执行下面的方法,锁是关闭的,则线程进入锁池等待。

5、消费者生产者问题

生产者线程生产物品,消费者线程消耗物品,但是生产者并不知道什么时候物品是空,消费者也不知道什么时候物品是有的。因此需要设计锁,使得生产者生产物品时,消费者不能进来打断,而当消费者消耗物品,生产者同样不能进来打断。生产者进来时如果已经有商品,就会执行wait()方法,使自己进入等待池,开启锁池,让消费者进来消费,消费者消费完,又会调用notify()方法,从等待池中释放生产者进入锁池去生产。

这个例子中,消费者生产者问题模拟的场景是取情报。一颗大树中间有个洞,生产者负责把情报放在洞里,消费者负责把情报取走,彼此并不知道什么对方什么时候放/取情报。示例代码如下:

package com.xqh.in;
public class Test1
{
	public static void main(String[] args)
	{
		Tree q=new Tree();
		Producer p=new Producer(q);
		Consumer c=new Consumer(q);
		p.start();
		c.start();
	}
}
class Producer extends Thread
{
	Tree q;
	Producer(Tree q)
	{
		this.q=q;
	}
	public void run()
	{
		for(int i=0;i<10;i++)
		{
			q.put(i);
			System.out.println("Producer put "+i);	   
		}
	}
}
class Consumer extends Thread
{
	Tree q;
	Consumer(Tree q)
	{
		this.q=q;
	}
	public void run()
	{
		while(true)
		{
			System.out.println("Consumer get "+q.get());
		}
	}
}
//树类
class Tree
{
	private int hole;//树洞
	boolean bFull=false;
	//放情报
	public synchronized void put(int i)//给方法上锁,同时间只能有一个线程调用put/get方法。因为是对同一个树上锁
	{
		if(!this.bFull)//如果为空
		{
			this.hole=i;//把情报放入树洞
			this.bFull=true;//设为非空
			/*从该对象的等待队列中释放消费者线程进入该对象锁池	
			使该线程将再次成为可运行的线程*/
		    this.notify();
		}
		try
		{//如果树洞里已经有情报了,让生产者线程进入this对象的等待队列,然后开启锁池让消费者来取
			this.wait();
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
			
	}
	//取情报
	public synchronized int get()
	{
		if(!this.bFull)//如果为空
			try
			{
				//开启锁池,自己进入等待队列。
				this.wait();//如果树洞里没情报,让消费者线程进入this对象的等待队列,然后开启锁池,让生产者进来
			}
			catch(Exception e)
			{
				e.printStackTrace();
			}
		//锁池:当线程到加锁的方法,此时锁关闭,线程就会进入锁池
		//等待池:一旦进入等待池,不能自动出来,要调用notify方法才能从等待池释放
		bFull=false;;//设置为空
		/*消费者取完情报,从该对象的等待队列中释放生产者线程进入该对象锁池
		使该线程将再次成为可运行的线程*/
		int value=this.hole;
		this.notify();
		return value;
	}
	//wait后自己进入等待池,必须等到对方notify,才能从等待池释放。
	
}

//陷入死循环。生产者放到9之后,生产者不再放情报,生产者线程死亡。但此时消费者并不知道,依旧
//执行线程,发现没有情报,于是执行wait让自己进入等待池,但此时再也没有生产者来唤醒它,于是
//陷入死循环。



输出结果:

Producer put 0
Consumer get 0
Consumer get 1
Producer put 1
Consumer get 2
Producer put 2
Consumer get 3
Producer put 3
Consumer get 4
Producer put 4
Consumer get 5
Producer put 5
Consumer get 6
Producer put 6
Consumer get 7
Producer put 7
Consumer get 8
Producer put 8
Consumer get 9
Producer put 9

创建两个线程对象,生产者线程对象p,消费者线程对象c;

生产者:

public synchronized void put(int i)

给方法上锁,这是放情报的方法。由生产者线程来调用,当生产者线程执行到这里,判断锁是否关闭,关闭则进入锁池等待,打开则执行调用方法;

this.notify();

如果树洞里为空,则放入情报(this.hole=i;),并将大树设为非空,然后调用this.notify(); ,意思是从该对象的等待队列中释放消费者线程进入锁池,使消费者线程再次成为可运行的线程。

this.wait();

如果生产者发现树洞里已经有情报了(this.bFull),则调用this.wait(),意思是让自己进入this对象的等待队列(等待消费者的notify唤醒),并开启锁池让消费者线程执行。

消费者:

public synchronized int get()

给消费者取情报的方法加锁。当消费者执行到这里,需要判断锁是否关闭,如果关闭则进入锁池等待,如果打开则执行get方法

this.wait()

如果树洞为空,那么消费者就没事可干,调用wait(),将自己线程进入等待队列,开启锁池,生产者线程占用处理机

this.notify()

如果树洞有情报,消费者将情报取走,将大叔设为空,然后调用this.notify(),意思是从该对象的等待队列中释放生产者线程进入锁池,使生产者线程再次成为可运行的线程

  • 为什么程序会进入死循环?

当生产者最后一次放入情报后,不再满足循环条件,线程结束,此时,消费者并不知道生产者已经不再投放情报,消费者执行到取情报代码,发现树洞为空,于是调用wait(),使其进入等待队列,但此时已经生产者来唤醒它了(notify()),于是程序会进入死循环。

  • 注意锁池和等待队列

每个对象不仅有锁池还有等待队列。进入等待队列的线程,无法自动出队列,只有等到线程notify()唤醒才能从等待队列释放进入锁池。而锁池里的线程,只要锁打开,就可以执行锁住的方法。

6、构造方法不能加锁,构造方法本身就属于线程安全的,不存在同步一说

6.2、ThreadLocal

1、ThreadLocal是什么

ThreadLocal叫做线程变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。每个Thread有自己的实例副本,且其他Thread不可访问,那就不存在多线程间共享的问题。提高并发性,之前多线程会对同一实例变量争用,导致数据混乱,用ThreadLocal则不会存在这个问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本 。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

2、ThreadLocal和synchronized区别

ThreadLocal和Synchonized都用于解决多线程并发访问,但有本质的区别:

  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
  • Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程 访问。而ThreadLocal为每一个线程都提供了变量的副本,不存在多线程间共享的问题,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。 而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

3、简单使用

public class ThreadLocaDemo {
 
    private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
               
            }
        },"A").start();
 
        Thread.sleep(1000);
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_B");
                print("B");
                System.out.println("after remove : " + localVar.get());
              
            }
        },"B").start();
    }
}
 
A :local_A
after remove : null
B :local_B
after remove : null
 

两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱.

ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则先实例化threadLocalMap,并将value值初始化。

说明:当线程处于活动状态时它会持有该线程的局部变量的引用, 当该线程运行结束后,该线程拥有的局部变量都会结束生命周期。

ThreadLocal类主要由四个方法组成initialValue(),get(),set(T),remove()。在ThreadLocal类中有一个线程安全的Map,用于存储每一个线程的变量的副本。key是ThreadLocal,value是我们设置的value。

6.3、线程死锁

1、线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

假设线程1先执行,通过锁,得到资源1后,休息1s(此时并没有打开锁),然后线程2执行,通过锁,得到资源2,休息1s(没有打开锁),然后线程1醒来,想要获取资源2,但是获取不到(因为锁是关闭的),而线程2醒来想要获取资源1,彼此都请求获取对方的资源,两个线程陷入互相等待的状态,产生死锁。

2、产生死锁的四个必要条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用
  • 请求与保持条件:一个线程因请求资源而阻塞时,对方获得的资源保持不变
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

3、如何预防和避免死锁

  • 预防死锁

破坏死锁产生的必要条件即可

  • 避免死锁

在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

6.4、乐观锁和悲观锁

1、悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题,所以每次在获取资源操作的时候都会上锁。共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程

Java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现

  • 缺点:

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量线程阻塞会导致系统的上下文切换(更换线程时,保存当前线程的上下文以及加载下一个线程的上下文),增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。

2、乐观锁

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改时去验证相对应的资源是否被其他线程所修改 。(一般用版本号机制/CAS算法实现)

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上更胜一筹。但是如果写占比很多的情况,会频繁失败和重试,同样会影响性能。

3、使用场合

理论上来说:

  • 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。
  • 乐观锁通常用于写比较少的情况下(多读场景,竞争较少),可以避免频繁加锁影响性能。

4、实现乐观锁

  • 使用版本号机制

一般是在数据表中加上一个数据版本号version,表示数据被修改的次数,当数据被修改时,version值会加1。例如,当线程A要更新数值时,在读取数据的同时也会读取version值,提交更新时,若刚才读取到的version值和当前数据库中的version值相等才更新(防止在读取期间其他线程对数据进行修改导致版本号不一致),否则重试更新操作。

  • CAS算法

CAS思想很简单,就是用一个预期值(这个变量的值)和要更新的变量值去进行比较,两值相等才会进行更新。

可能存在问题:

ABA问题,如果一个变量V初次读取是A值,并且准备赋值时它仍然是A值,能说明它的值就一定没有被修改吗?是不能的,因为在这段时间,可能其他线程对它修改值为B,然后又改回A,那么CAS就会认为它从来没有被修改过。—解决方案加上版本号

6.5、ReentrantLock

1、ReentrantLock是什么

ReentrantLock实现了Lock接口,是一个可重入且独占式的锁,和synchronized关键字类似。不过,ReentrantLock更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

2、公平锁和非公平锁区别

  • 公平锁:锁被释放后,先申请的线程先得到锁,锁性能差一点,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但是可能导致有些线程永远获取不到锁。

七、volatile关键字

1、在java中,volatile关键字可以保证变量的可见性。如果我们将变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

  • volatile关键字能保证数据的可见性,但不能保证数据的原子性,synchronized关键字两者都能保证

2、在java中,volatile关键字除了可以保证变量的可见性,还有一个重要的作用就是防止JVM的指令重排序。

3、volatile关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。

4、volatile和synchronized的区别

  • volatile关键字是线程同步的轻量级实现,所以性能肯定比synchronized要好。但是volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性

你可能感兴趣的:(javase,java,学习,python)