看了很多进程和线程的差别。用教科书上的讲法可以写好几百行,但说白了有点基础之后,会发现其实它们只有一个差别——进程有独立的内存,线程共享所处进程的内存。其他差异都是源于这个。
为什么要有线程、进程的概念。
就是为了榨干机器的性能。比方说买了台最高配置的电脑,但只用它打字,那就浪费了。文字软件只占一点点资源。所以,要同时用来打字、听歌、打游戏、下载。
既然有这么多同时使用的需求,所以就需要将资源打包。打包完的就是进程。
进程获得了cpu和内存资源,但是比如一个音乐软件,它也同时具备播放和歌词的功能,需要多任务。那么这时候就有了线程,线程是执行任务的最小单位。
线程麻烦的地方在于,它们要合作。所以就会有资源争用。
线程a,线程b,共同使用资源a,资源b。
这里的线程a就是一个线程。但是资源a可能是一组资源。如果资源a和资源b之间有共同的“有边界的子资源”,那么在使用过程中就必须要加锁。由于加锁之后只能有一个线程使用资源,所以锁的粒度还要依据场景控制,尽量让锁的范围越小越好,这样有助于提升资源使用率。
举个例子,依然是经典的银行转账。
1.线程a使用资源a,将用户甲的100元转给用户乙。此时操作是甲-100,乙+100。
2.线程b使用资源b,将用户甲的100元转给用户丙。此时操作是甲-100,丙+100。
3.再加一个限制,甲一共只有100。
在这个情况下,转账前就必须做一下验证,甲的账户是不是还有100元的余额。
于是线程a要做这些事:
1.验证甲账户余额是否大于100.
2.甲账户扣减100.
3.乙账户加100.
同样线程b要做这些事:
1.验证甲账户余额是否大于100.
2.甲账户扣减100.
3.丙账户加100.
在线程a和线程b的1,2步骤,必须是原子操作。否则就会出现线程a执行完了步骤1,立刻切换到线程b执行完了步骤1.发现这个校验都没问题,于是进行扣减,一扣就出了问题。
把1和2两步合并为一步,我们就需要构造这么一个原子的逻辑组合。这个逻辑组合,就是【管程】。
理解【管程】就理解了所有的锁。
所以现在程序变成了这样子:
pre1.加锁
1.验证甲账户余额是否大于100.
2.甲账户扣减100.
after2.解锁
3.乙账户加100.
在pre1和after2之间的,就是管程。
那Java中是如何实现的呢。就是一个关键字:Synchronized。
Sycchronized关键字可以视作一个动作:用什么给什么上锁。
我们细化:用XX给XX上锁。
第二个XX我们都很熟悉了,就是上面讲的管程,因为用到了锁的概念,所以也可以将其比喻为一个房间。
第一个XX也很好理解,就是锁。
所以整句话就是,用锁给房间上锁。
技术中有劲的就是,每个概念都可以再分。
所以对于锁这个概念,就又存在锁体和钥匙的区分。一个锁体,配一个钥匙。
所以代码写代码的时候,我们对线程操作的时候,要千万小心我们在用什么上锁。所以写synchronized(this)的时候,就是在用当前的“对象”上锁,那就容易上错了。线程1用锁1去锁资源a,那么下一个线程调用资源a时,就要判断是不是锁1,如果是用1在锁,但是判断的却是是否存在锁2,那么线程2就依然畅通无阻地进入管程。
加锁最怕的不就是锁不上。要注意这点。
咋避免。在更大的上下文里找一个唯一的对象作为锁,就是更合适的方式。
还有要吐槽的一天是,有时候面试会问Thread和Runnable的差别。个人感觉没有差别。与其问这个问题,不如问继承和实现的差别。
下面会讲讲callable和runnable,这俩还是不大一样的。
网友评论