关于多线程同步的一些理解

简介:简单介绍一下同步和资源共享。

同步

关于同步这个名词,个人理解是这样的:同步并不是指同时进行,而是协同步调,让双方或者多方能够共同把事情做好而不是各方乱作一团。
有的时候,比如"两人三足"游戏,这里的同步,确实就是同时进行,每个人都有相同的节奏;
但有的时候,比如"两人共同砸年糕",这里的同步,就是指两个人交替执行自己的动作,并且节奏相符,不发生碰撞。
在多线程中,同步就是指多个线程之间协同运行,而不引发一些数据的混乱和逻辑的错误。
而线程之间,最常见到的一种需要同步的场景,就是操作共享资源的时候。

资源共享

比如,现在有一个int变量num,初值为0。我们还有一个累加器程序,现在开启两个线程,都执行这个累加器的累加方法,该方法就是把一个数加5万次,那么最终这个数应该是十万。代码如下:

public class Test {
	public static void main(String[] args) {
		Count c = new Count();
		Thread t1 = new Thread(c,"★1");
		Thread t2 = new Thread(c,"★2");
		t1.start();t2.start();
		//让主线程睡一会,保证主线程的最后一条输出语句是最后执行的
		try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}
		System.out.println(c.num);
	}
}
class Count implements Runnable{
	int num = 0;
	@Override
	public void run() {
		f();
	}
	public void f() {
		for (int i = 0; i < 5 * 10000; i++) {
			num++;
		}
	}
}

这里我们看到,新建的两个线程t1,t2都是由同一个Count对象构造出来的,所以这两个线程是共享着这个对象的所有数据。(顺便说一下,实现Runnable能够更方便得共享数据的原因就在这里,继承Thread类,是不能达到这种效果的)。
执行结束后,输出的结果在5~6万左右。这个就是因为线程之间没有进行很好的同步,当一个线程进行(读–累加–写)操作的时候,总会有其他线程来干扰它,让一个简单的(读–累加–写)变成了类似于(1号读--2号读--2号累加--2号写--1号累加--1号写)这样的情景。所以多个线程之间是不同步(协同)的。为了解决这个问题,我们可以在f()方法中的循环体外,加上synchronized关键字,来保证一个线程进行(读--累加--写)操作的时候,不能有其他线程来干扰,代码如下:(被synchronized大括号所包含的代码,就被成为同步代码块

public void f() {
	synchronized (this) {
		for (int i = 0; i < 5 * 10000; i++) {
			num++;
		}
	}
}

这样一来,输出的结果就是10万了。
这时候就不得不提一下synchronized关键词后面的小括号。先来看这样一个例子,有2个小孩,还有一个擎天柱玩具和一个大黄蜂玩具,每个玩具同一时间只能被一个人玩。所以,当某个人抢到某个玩具的时候,就要拿到"玩擎天柱令牌",这样玩玩具的过程就不会被干扰。但是A小孩玩擎天柱,并不影响B小孩玩大黄蜂,所以,擎天柱的令牌和大黄蜂的令牌,不能是同一个。那么在程序中,是如何区分不同的令牌的呢,没错,就是使用synchronized后面的小括号,放入不同的东西,就代表了不同的令牌。
那么可以放入哪些东西呢,答案是只要是对象(数据类型为Object)就都可以。所以我们可以利用任何一个对象,来把它当成一个令牌(就像2个小孩可以规定"空调遥控器"就是擎天柱的令牌,只有拥有特定空调遥控器的人,才能玩擎天柱)。而有些时候,我们只需要一个令牌,专门new一个对象出来当令牌又很奢华和麻烦,又由于我们的函数是成员函数,所以,想要调用此方法,就必要有一个对象,那么我们就把这个未来的对象(也就是this),来当作令牌。上文中的代码,就是使用的这种方式。(如果方法是静态方法,那么可以传入该类的Class对象来当作锁)。
又有的时候,我们函数的所有代码,全部都被放在了synchronized同步代码块中,我们就可以把synchronized关键词放到函数的返回值前,从而达到一样的效果,此时默认使用的令牌就是this对象。这就是同步方法

public synchronized void f() {
	for (int i = 0; i < 5 * 10000; i++) {
		num++;
	}
}

线程间的通信

当然,对于相同或相似业务逻辑以及共线资源简单的几个线程,我们可以使用如上的方法。但遇到复杂的情况,我们就需要使用一些方式在让线程间相互传递消息。比如经典的生产者和消费者问题:
假设有10个线程,五个生产者,五个消费者,和一个缓冲区。很明显,生产者和消费者的业务逻辑是不同的,而他们10个,都是去操作着同一个资源——缓冲区,所以为了让他们10个共享此区域,可以把放一个缓冲区类型的指针当作他们的成员变量,然后在新建生产者或消费者对象的时候,传入同一个缓冲区对象,就可达到资源共享的目的。
实现资源共享以后,要考虑这样一个问题,以消费者为例,当缓冲区中产品数量不足时,那么这个消费者就应该释放令牌,给生产者拿到令牌的机会,从而让生产者能够往缓冲区中添加产品。实现这一个操作的函数就是wait(),要注意的是,为了避免"一些问题",Java要求该方法必须配合同步块使用。当调用了该方法,那么此线程就会释放锁,并进入等待状态(等待状态时,是没有资格重新参与令牌的竞争的)。那么想从等待状态出来,就需要另外一个函数——notify(),该函数就可以唤醒处于等待态的一个线程,让他重新有资格参与令牌的竞争(不是直接把令牌可他,只是又有资格去竞争了)。这里我们就可以看出一个对象(令牌),是有两种领域的:在这里插入图片描述
当一个线程调用了wait(),那么他就会进入Wait Set中,在这里的线程,是没有资格争夺令牌的。而拥有令牌的线程,如果运行了notify()函数,那么就是把Wait Set中的某一个移动到Entry Set中,而Entry Set中的线程,是有资格进行令牌争抢的。还要一个函数是notifyAll(),它的作用自然就是把Wait Set中的所有线程,都放到Entry Set中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值