什么是多线程?
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
单核CPU时:有多个线程,A线程先运行一段时间然后切换成B线程,如此继续切换。中间切换时会有卡顿,但是只占很短的时间片,在用户眼中,就好像有多个线程在执行。
多核CPU时:多个线程确实在并发运行,每个CPU运行一个线程。如果不同CPU上的线程之间有信息需要同步,当同步时,需要将信息从一个CPU的存储空间复制去另外一个CPU的存储空间,会有一定开销。(CPU处理数据时,也是先把内存的数据拿到CPU的存储空间再进行处理)
注:我们编程的时候还是按照单线程编程,但是框架处理的时候会根据具体情况调用单线程或多线程处理
单核CPU什么时候用多线程?
单核CPU的情况下,如果频繁切换线程,因为中间会有暂停时间片的原因,多线程运行的时间会大于单线程运行的时间。
那么,我们在单核的情况下,究竟什么时候应该用多线程呢?
1.应用情况:耗时或大量占用处理器的任务阻塞用户界面操作,使用多线程。
2.性能情况:各个任务必须等待外部资源,就是当CPU被浪费时使用多线程。比如发送一个请求,等待响应的时间就是被浪费的,这个时间可以用来运行其他线程
多线程的不利方面:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要CPU时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
线程太多会导致控制太复杂,最终可能造成很多Bug;
单核CPU多线程的线程状态
JAVA多线程中,线程何时进入阻塞状态?
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行。知道线程进入就绪状态(Runnable),此时才有机会转入到运行状态。阻塞状态分为以下几种:
1. 等待阻塞(watiting):运行的线程执行wait()方法,JVM会把该线程放入等待池中。
2. 超时等待阻塞(time-waiting):就是sleep(),运行时的线程会执行带有超时时间的wait()方法,JVM会把该线程放入等待池中。当超时后,线程重新转入就绪状态。
3. 同步阻塞(synchronized):运行时的线程获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
4. 其他阻塞:运行的线程执行sleep或者join方法,或者执行I/O请求时,JVM会把该线程置为阻塞状态,当sleep状态超时,join等待线程终止或者超时是,线程重新转入就绪状态。
sleep()和wait方法是否持有锁?
sleep是Thread类的方法,导致此线程暂停执行指定时间,但是依然保持着监控状态,过了指定时间会自动恢复(CPU中有一个时间戳标记)。锁是用来线程同步的,sleep(long mills)虽然让出了CPU,但是不会让出锁,其他线程可以利用CPU时间片了,但如果其他线程要获取sleep(long mills)拥有的锁才能执行,则会因为无法获取锁而不能执行,继续等待。
当调用sleep方法后,当前线程进入阻塞状态。目的是让出CPU给其他线程运行的机会。但是由于sleep方法不会释放锁对象,所以在一个同步代码块中调用这个方法后,线程虽然休眠了,但其他线程无法访问它的锁对象。这是因为sleep方法拥有CPU的执行权,它可以自动醒来无需唤醒。而当sleep()结束指定休眠时间后,这个线程不一定立即执行,因为此时其他线程可能正在运行。
wait方法是Object类里的方法,当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时释放了锁对象,等待期间可以调用里面的同步方法,其他线程可以访问,等待时不拥有CPU的执行权,否则其他线程无法获取执行权。当一个线程执行了wait方法后,必须调用notify(随机唤醒一个等待该资源的线程,具体唤醒谁由操作系统决定)或者notifyAll方法才能唤醒,而且是随机唤醒,若是被其他线程抢到了CPU执行权,该线程会继续进入等待状态。由于锁对象可以时任意对象,所以wait方法必须定义在Object类中,因为Obeject类是所有类的基类。
注意:
sleep()睡眠时间只是大概的时间,并不一定精确。如果此时切换到的其他线程进行某些操作的时候,很可能会造成睡眠时间延后,但是其等待时间越长,优先级越高。
sleep和wait唤醒后,都是从上次中断的地方继续执行
多个线程想访问上锁的区域时,若发现锁被一个线程占用,会不断的询问CPU锁是否被释放。多个线程宏观来说是同时询问,而实际上是CPU分配时间片询问。
wait()和notify()的通常用法
Java多线程开发中,我们常用到wait()和notify()方法来实现线程间的协作,简单的说步骤如下:
1. A线程取得锁,执行wait(),释放锁;
2. B线程取得锁,完成业务后执行notify(),再释放锁;
3. B线程释放锁之后,A线程取得锁,继续执行wait()之后的代码;
notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
如何线程同步?
join()和synchronized(),join内部实现也是synchronized,但是join开销大。
注意:要在线程启动后调用
oo
这把锁,t2
线程先持有,t2
执行完才开始执行t1
。join
方法会造成当前线程wait
,就如你看到的这里的wait(0)
,是当前线程wait
,并不是调用者wait
,正如join
方法的说明一样,Waits for this thread to die. 你的程序里,就是说主线程等到t1
线程执行完以后再执行,主线程的wait
状态,应该是由t1
执行完成之后调用的notify
解除。
加锁一定要尽量锁住的区域小,不然导致其他线程等待时间会过长
线程如何互斥访问资源?
为这个共享变量约定一个信号量。当前线程不满足该条件时,将他们wait,去执行其他线程。此线程要等满足条件后再唤醒。
多线程和多进程的区别
我们比较的是同一个程序里的多线程和多进程。
多线程因为在同一个进程里,所以可以共享内存和其他资源,比如迅雷里10个线程一齐下载一个文件,这个文件是由进程打开的,然后10个线程都可以往里写入东西。如果是10个进程就不行了,操作系统不允许一个文件由两个进程同时写入。
另外,Chrome就是一个典型的多进程程序,里面每个标签页、扩展、插件都是单独的进程,各自独占资源,相互隔离,一个进程出错死掉只会影响一个页面或者插件,再也不会出现Flash插件出错崩溃导致整个浏览器崩溃的情况了(一个线程崩溃不一定进程崩溃)。