Java基础学习笔记——多线程(2)

1. 线程安全问题

1.1 产生原因

  1. 不同的线程操作共享数据
  2. 操作共享数据的语句代码有多条
  3. 有以上两条,可能这个线程执行完判断语句还没有执行下面的语句的时候,切换到别的线程了,这样就会出现问题

1.2 解决办法

主要思想是,只要让某一代码块持有同一个对象(锁),那么多线程调用的时候就能保证这一块代码的原子性。这个锁对象,可以是多线程操作的同一个对象内部自己定义的,可以是这个多线程操作的这个对象本身,可以是这个对象对应的字节码文件对象

同步代码块

对于那些操作共享数据的代码,我们将其封装,可以加上synchronized(同步)关键字标识的同步代码块。其实原理就是相当于加了个锁,当某个线程进入到这个语句中的时候,持有同步锁,如果执行到一半切换到其它线程的时候,其它线程拿不到锁,只能在外面干等着。
格式:

Object obj=new Object();
//run方法外部的同步锁,也必须公有
//run方法内部的同步代码块
synchronized(obj){
	//需要同步的代码
}

同步函数

同步代码块和函数一样,都能封装代码,那让函数直接具有同步的功能不就行了吗。所以这里就到了同步函数,在函数定义的时候加个synchronozed修饰符就行了
同步函数使用的锁是this,相当于synchronized(this){代码},所以同步函数与同步代码块相比更简单,但是功能也有了限制。

静态同步函数

java对字节码文件封装了对象,当类加载进内存的时候,堆内存里其实就有了对应的字节码文件对象。静态同步函数调用的时候,需要一个锁(锁必须是对象),虽然堆内存里没有对象,但是这个锁就是那个字节码文件对象。
相当于synchronized(this.getClass())或者synchronized(Ticket.class)

1.3 单例设计模式的多线程问题

对于饿汉式,不存在多线程安全问题,但是对于懒汉式,因为要判断一下是否堆内存有对象那一步,所以就会出现安全问题。可以这样解决:

class Single{
	private static Single s=null;
	private Single(){}
	public static Single getInstance(){
		if(s==null){
			synchronized(Single.class){
				if(s==null)s=new Single();
			}
		}
		return s;
	}
}

注意这里如果直接把这个得到实例的静态函数声明为synchronized的话,每次获取对象都会判断锁而降低效率,而如果像上面一样定义,则会提高效率。

2. 线程间通信

之前的情况是多个线程运行相同的任务代码,处理相同的资源,现在的情况考虑多个线程处理同一个资源,但是任务代码却不同。

2.1 简单的示例代码

考虑下面这种情况,两个线程处理同一个资源,一个线程负责写入,一个线程负责读取。

  • Resource:
public class Resource {
	public String name;
	public String sex;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getSex() {
		return sex;
	}
	public void setSex(String sex) {
		this.sex = sex;
	}

}

  • InputSource(负责写入数据):
public class InputSource implements Runnable{
	private Resource r=null;
	private int flag=0;
	

	public InputSource(Resource r) {
		super();
		this.r = r;
	}


	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			synchronized(this){
				if(this.flag==0) {
					r.setName("小明");
					r.setSex("男");
				}
				else {
					r.setName("小红");
					r.setSex("女");
				}
					
			}
			this.flag=(this.flag+1)%2;
			
		}
		
	}

}
  • OutputSource(负责读取数据):
public class OutputSource implements Runnable {
	private Resource r=null;
	

	public OutputSource(Resource r) {
		super();
		this.r = r;
	}


	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			synchronized(this) {
				System.out.println(r.getName()+" : "+r.getSex());
			}
		}
		

	}

}

  • 主线程:
public class AccessResource {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Resource source=new Resource();
		Thread inputit=new Thread(new InputSource(source));
		Thread outputit=new Thread(new OutputSource(source));
		inputit.start();
		outputit.start();

	}

}

对于这个资源,我们不断的写入小明 男小红 女,然后不断的读取这两个数据,上述代码的同步锁是不同样的,所以对于数据的读写并没有得到有效的保护,所以可能会得到类似下面的结果:


小明 : 女
小明 : 女
小红 : 男
小红 : 女
小明 : 女
小明 :

当一个线程刚把名字改了,性别还没改的时候,另一个线程就直接把这个新名字和旧性别给输出了,这显然是不符合常理的。
这时候,我们可以将两个线程里的同步锁都设置为this.r,指向同一个资源对象,这时候就不会出现上述“人妖”的问题了

2.2 等待/唤醒机制

相关方法

wait():将本线程转换到睡眠状态,即交出CPU执行资格和执行权,存储到线程池(等待集)中
notify():将线程池中的线程随机唤醒一个
notifyAll():将线程池中的所有线程唤醒

  1. 它们继承自Object类,必须要由锁对象调用
  2. java中有多个线程,多个线程会有不同的同步代码块,而锁,便是把这些线程分组的一个东西。拥有相同的锁的同步代码块(不管你这个同步代码块相同不相同,属不属于同一个run方法)这些代码块所属的线程是必须保证里面的代码的原子性的,而上述的三种方法,操作的是这些线程,而不是全部的线程。
  3. 锁就像个监视器,监视线程,负责把关,上述方法也叫作监视器方法
  4. 不同的锁对应不同的线程池
  5. 对于一个同步代码块,不一定里面只有一个活着的线程,可以有多个活着的线程!比如使用wait()让多个线程等待,之后其它线程又使用notifyAll()把这几个线程同时复活,此时这几个线程就处于活着,并且在同一个同步代码块的状态!但是这并不意味着他们会同时执行,只有拿到锁的线程才会执行,拿不到的还是会继续等待。这个例子主要是明确一点,处于活着状态的线程不一定总是处在同步代码块之外。

改进后的示例代码

将姓名与性别交替赋值输出,改动代码如下:

  • Resource:
public class Resource {
	public String name="beginname";
	public String sex="beginsex";
	public boolean flag=false;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getSex() {
		return sex;
	}
	public void setSex(String sex) {
		this.sex = sex;
	}
	public synchronized void setMessage(String name,String sex) throws InterruptedException {
		if(this.flag==false)this.wait();
		this.setName(name);
		this.setSex(sex);
		this.flag=false;
		this.notify();
	}
	public synchronized void printMsg() throws InterruptedException {
		if(this.flag==true)this.wait();
		System.out.println(this.getName()+" : "+this.getSex());
		this.flag=true;
		this.notify();
	}

}
  • InputResource:
public class InputSource implements Runnable{
	private Resource r=null;
	private int identityFlag=0;

	public InputSource(Resource r) {
		super();
		this.r = r;
	}


	@Override
	public void run() {
		// TODO Auto-generated method stub
		try {
			while(true) {
				if(this.identityFlag==0)r.setMessage("小明", "男");
				else r.setMessage("小红", "女");
				this.identityFlag=(this.identityFlag+1)%2;
				
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

}
  • OutputResource:
public class OutputSource implements Runnable {
	private Resource r=null;
	

	public OutputSource(Resource r) {
		super();
		this.r = r;
	}


	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			try {
				r.printMsg();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		

	}

}

  • 主线程:
public class AccessResource {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Resource resource=new Resource();
		Thread t1=new Thread(new InputSource(resource));
		Thread t2=new Thread(new OutputSource(resource));
		t1.start();
		t2.start();
		

	}

}

wait和sleep区别

  • wait:可以跟时间,可以不跟时间,释放CPU执行权,释放锁
  • sleep:必须跟时间,释放CPU执行权,不释放锁

3. 线程调用的方法

停止线程

interrupt()方法,可以将线程从冻结状态强制恢复到运行状态来,让线程具备CPU的执行资格,也就是说不管我是sleep还是wait,我都突然原地复活,由于这个方式太过暴力,所以每次调用这方法都会抛出InterruptException,当抛出这个异常的时候,可以考虑在catch里面设置停止线程的各种方法

守护线程

或者说后台线程,需要启动前声明

t1.setDaemon(true);
t1.start();

对于前台线程来说,某个线程的消失对另一个线程没任何关系,如果不手动结束,会一直按照代码执行。但是将某个线程声明为守护线程之后,前台线程结束后,该线程就会自动结束,无论冻结与否。
之所以叫做守护,是因为如果前台线程没了的话,我也没有守护的意义了,只好随前台而去

join

  1. 在主线程内部启动线程0后,如果再使用t.join(),那么主线程会冻结(可以interrupt),等待这个线程执行完之后再执行,在这之前没有CPU执行资格
  2. 在一个线程内部临时加入一个线程,使用join方法

setPriority

设置线程的优先级,最大为10,默认都是5

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值