区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 | 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程都有独立的运行栈和程序计数器(PC),线程切换开销小 |
所处环境 | 在操作系统中能同时运行的多个任务(程序) | 在同一应用程序中有多个流同时执行 |
分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域 | 除了 CPU 之外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源 |
包含关系 | 没有线程的进程是可以被看做是单线程的,如果一个进程内拥有多个线程则执行过程不是一条线的,而是多条线(线程)共同完成的 | 线程是进程的一部分,所以线程有的时候会被称为是轻权进程或者轻量级进程 |
public class ThreadDemo extends Thread {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"---------"+i);
}
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+"=========="+i);
}
}
}
注意:Thread 类中的 run 方法是存储线程要运行的代码,主线程要运行的代码存放在 main 方法中
start 方法是开启线程并执行该线程的 run 方法
继承 Thread 类方式的缺点:
如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类
如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类
如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类
public class RunnableDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+"---------------"+i);
}
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+"===================="+i);
}
}
}
使用 Runnable 接口实现多线程优点:
可以实现继承。实现 Runnale 接口的方式要通用一些
可以实现继承。实现 Runnale 接口的方式要通用一些
可以实现继承。实现 Runnale 接口的方式要通用一些
代理模式主要使用了 Java 的多态,干活的是被代理类,代理类主要是接活,你让我干活,好,我交给幕后的类去干,你满意就成,那怎么知道被代理类能不能干呢?同根就成,大家知根知底,你能做啥,我能做啥都清楚的很,同一个接口呗。
Thread 与 Runnable 的子类都实现了 Runnable 接口,之后将 Runnable 的子类 MyThread 的子类 MyThread 放到 Thread 类之中,测试类调用是 Thread 类中的 start 方法去启动多线程,实际上具体的执行者是 Runnable 的子类 MyThread 中的 run 方法中的代码
真实角色:MyThread
代理角色:Thread
实现共同接口:Runnable
注意:在多线程时候,可以实现唤醒和等待的过程,但是唤醒和等待操作对应的类不是 thread,而是我们设置的共享对象或者共享变量(Object 类中的方法)
(此处参照计算机操作系统一书中进程同步的概念,我把进程替换成了线程)
线程同步的主要任务是对多个相关线程在执行次序上进行协调,是并发执行的诸进程之间能按照一定的规则(或时序)共享资源,并能很好的合作,从而使程序的执行具有可再现性。
线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系。
线程同步其实实现的就是线程排队,防止线程同步访问共享资源造成冲突,变量需要同步,但是常量不需要,因为常量存放于方法区。
只要这些线程的代码访问同一份可变的共享资源,这些进程之间就需要同步。
如果没有线程同步操作,将会产生非常严重的后果。
举个生活中常见的例子:
小红和小绿是一对夫妻,小红每个月会给小绿的银行账户中存入 1000 元钱作为小绿的生活费,有一天小红误操作存了 2000 进去,此时小绿正在查看账户余额忽然发现比平时钱多了一倍,喜出望外非常感动立马准备取钱出来准备去吃顿自助餐。与此同时小红也发现自己的操作失误,准备取出多出来的 1000 元钱。注意:此处小红和小绿同时取钱操作,是纳秒级别的并发操作。而由于银行系统没有进行线程同步操作。此时会发生什么?
小绿成功的取出 2000 块钱,小红成功的取出多转的 1000 块钱。
明明卡里只有两千块钱,小红和小绿却取出了总金额 3000 元。这么干下去,银行早倒闭了。
而此时银行的程序员小六立马发现了这个漏洞,开始考虑解决方案,都说程序员个个都智商绝顶(没有冒犯的意思),很快想出了一个聪明的办法来解决这个八阿哥,既然是由于并发产生的问题,那么我让它不并发不就好了。
当超过一个人同时进行取款操作时候,对这个账户余额上一把锁,同一时间(瞬时)只能让一个人进行操作,其他人排队等待。当第一人操作完成之后释放锁,然后第二个人才能开始操作。
当小红和小绿发现这个财富密码之后,就又开始了薅资本主义羊毛的骚操作,这次还是同时取款,小红和小绿发现这次操作时候自动取款机的程序好像变慢了一点(显示器上显示》》》》操作中请等待》》》》),这次是小绿先取出钱,然后小红的操作界面显示余额不足。此次小红和小绿的薅羊毛行动失败了。
果然 排队 这个方法非常有效,再也没发生过这种事情,解决这个 bug 的代价只是让程序看起来慢了一丢丢而已,这对银行来说成本几乎可以忽略不计,于是银行的程序员小六很快就升职加薪并且找到了同在一个银行上班的小红做自己的女朋友。
此处仅仅是线程基础内容,不会引出太多内容,不然这一个点挖出来的东西我也写不完(我还没学会呢)
/**
* @Description TODO 同步方法
* @Author Fedeline
* @Date 11/19/20 12:22 PM
*/
public class TicketRunnable3 implements Runnable {
private int ticket = 10;
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sail();
}
}
public static void main(String[] args) {
TicketRunnable3 ticketRunnable = new TicketRunnable3();
Thread t1 = new Thread(ticketRunnable,"A");
Thread t2 = new Thread(ticketRunnable,"B");
Thread t3 = new Thread(ticketRunnable,"C");
Thread t4 = new Thread(ticketRunnable,"D");
t1.start();
t2.start();
t3.start();
t4.start();
}
public synchronized void sail(){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
/**
* @Description TODO 同步代码块
* @Author Fedeline
* @Date 11/19/20 12:22 PM
*/
public class TicketRunnable2 implements Runnable {
private int ticket = 10;
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
}
public static void main(String[] args) {
TicketRunnable2 ticketRunnable = new TicketRunnable2();
Thread t1 = new Thread(ticketRunnable,"A");
Thread t2 = new Thread(ticketRunnable,"B");
Thread t3 = new Thread(ticketRunnable,"C");
Thread t4 = new Thread(ticketRunnable,"D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
死锁的起因
死锁的起因,通常是源于多个线程对资源的整多,不仅对不可抢占资源进行争夺时会引起死锁,而且对可消耗资源的进行争夺时,也会引起死锁。
在一组线程发生死锁的情况下,这组死锁进程中的每一个进程 ,都在等待另一个死锁进程所占用有的资源。或者说每个线程所等待的事件是该组中其它线程释放所占有的资源。但是由于所有这些进程已都无法运行,因此它们谁也不能释放资源,致使没有任何一个进程可被唤醒。这样这组进程只能无限期等待下去。
同样参照计算机操作系统一书中的定义
如果每一组线程中的每个线程都在等待仅由该组线程中的其他线程才能引发的事件,那么该组进程是死锁的(DeadLock)
以下四个比必要条件必须同时具备才会形成死锁
四个必要条件只要有一个被破坏就可以预防死锁