【Java中级】(四)多线程

线程的概念
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。 但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。 但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。 这就是进程和线程的重要区别。
4.1、Java创建一个线程的两种方式
1. 继承Thread类
2. 实现Runnable接口

注: 启动线程是start()方法,run()并不能启动一个新的线程
4.1.1、继承Thread类
package multiplethread;
import charactor.Hero;
public class KillThread extends Thread{
private Hero h1;
private Hero h2;
public KillThread(Hero h1, Hero h2){
this.h1 = h1;
this.h2 = h2;
}
public void run(){
while(!h2.isDead()){
h1.attackHero(h2);
}
}
}

package multiplethread;
import charactor.Hero;
public class TestThread {
public static void main(String[] args) {
Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;
Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;
Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;
Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;
KillThread killThread1 = new KillThread(gareen,teemo);
killThread1.start();
KillThread killThread2 = new KillThread(bh,leesin);
killThread2.start();
}
}
4.1.2、实现Runnable接口
package multiplethread;
import charactor.Hero;
public class Battle implements Runnable{
private Hero h1;
private Hero h2;
public Battle(Hero h1, Hero h2){
this.h1 = h1;
this.h2 = h2;
}
public void run(){
while(!h2.isDead()){
h1.attackHero(h2);
}
}
}

package multiplethread;
import charactor.Hero;
public class TestThread {
public static void main(String[] args) {
Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;
Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;
Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;
Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;
Battle battle1 = new Battle(gareen,teemo);
new Thread(battle1).start();
Battle battle2 = new Battle(bh,leesin);
new Thread(battle2).start();
}
}

4.2、常见线程方法
关键字
简介
sleep
当前线程暂停
join
加入到当前线程中
setPriority
线程优先级
yield
临时暂停
setDaemon
守护线程

4.2.1、当前线程暂停
Thread.sleep(1000); 表示当前线程暂停1000毫秒 ,其他线程不受影响
Thread.sleep(1000); 会抛出InterruptedException 中断异常,因为当前线程sleep的时候,有可能被停止,这时就会抛出 InterruptedException
4.2.3、加入到当前线程中
首先解释一下 主线程的概念
所有进程,至少会有一个线程即主线程,即main方法开始执行,就会有一个 看不见的主线程存在。
在44行执行t.join,即表明 在主线程中加入该线程
主线程会等待该线程结束完毕, 才会往下运行。
package multiplethread;
import charactor.Hero;
public class TestThread {
public static void main(String[] args) {
final Hero gareen = new Hero();
gareen.name = "盖伦";
gareen.hp = 616;
gareen.damage = 50;
final Hero teemo = new Hero();
teemo.name = "提莫";
teemo.hp = 300;
teemo.damage = 30;
final Hero bh = new Hero();
bh.name = "赏金猎人";
bh.hp = 500;
bh.damage = 65;
final Hero leesin = new Hero();
leesin.name = "盲僧";
leesin.hp = 455;
leesin.damage = 80;
Thread t1= new Thread(){
public void run(){
while(!teemo.isDead()){
gareen.attackHero(teemo);
}
}
};
t1.start();
//代码执行到这里,一直是main线程在运行
try {
//t1线程加入到main线程中来,只有t1线程运行结束,才会继续往下走
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Thread t2= new Thread(){
public void run(){
while(!leesin.isDead()){
bh.attackHero(leesin);
}
}
};
//会观察到盖伦把提莫杀掉后,才运行t2线程
t2.start();
}
}
4.2.4、守护线程
守护线程的概念是: 当一个进程里,所有的线程都是守护线程的时候,结束当前进程。

就好像一个公司有销售部,生产部这些和业务挂钩的部门。
除此之外,还有后勤,行政等这些支持部门。

如果一家公司销售部,生产部都解散了,那么只剩下后勤和行政,那么这家公司也可以解散了。

守护线程就相当于那些支持部门,如果一个进程只剩下守护线程,那么进程就会自动结束。

守护线程通常会被用来做日志,性能统计等工作。
package multiplethread;
public class TestThread {
public static void main(String[] args) {
Thread t1= new Thread(){
public void run(){
int seconds =0;
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.printf("已经玩了LOL %d 秒%n", seconds++);
}
}
};
t1.setDaemon(true);
t1.start();
}
}
4.3、多线程同步
多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题
多线程问题,又叫 Concurrency问题

同步问题产生的原因

解决思路:
总体解决思路是: 在增加线程访问hp期间,其他线程不可以访问hp

4.3.1、synchronized同步对象概念

如下代码:
Object someObject =new Object();
synchronized (someObject){
//此处的代码只有占有了someObject后才可以执行
}

synchronized表示当前线程,独占 对象 someObject
当前线程 独占了对象someObject,如果有 其他线程试图占有对象someObject,就会等待,直到当前线程释放对someObject的占用。
someObject 又叫同步对象,所有的对象,都可以作为同步对象
为了达到同步的效果,必须使用同一个同步对象

释放同步对象的方式: synchronized 块自然结束,或者有异常抛出



4.3.2、在方法前,加上修饰符sycronized
在方法前,直接加上sycronized,其所对应的的同步对象,就是this
外部现成访问该方法时,就不需要额外使用syncronized了

4.3.3、线程安全的类
如果一个类,其 方法都是synchronized修饰的,那么该类就叫做 线程安全的类

同一时间,只有一个线程能够进入 这种类的一个实例去修改数据,进而保证了这个实例中的数据的安全(不会被多个线程修改而变成脏数据)

4.4、常见的线程安全相关的面试题
4.4.1、HashMap和HashTable的区别
HashMap和HashTable都实现了Map接口,都是键值对保存数据的方式
区别1:
HashMap可以存放null
HashTable不可以存放null
区别2:
HashMap不是线程安全的类
HashTable是线程安全的类

4.4.2、StringBuffer和StringBuilder的区别
StringBuffer是线程安全的类
StringBuilder不是线程安全的类

所以进行大量字符串拼接的时候,如果是单线程就用StringBuilder会更快些,如果是多线程,就需要用StringBuffer保证数据的安全性

4.4.3、ArrayList和Vector的区别
Vector是线程安全的类
ArrayList不是线程安全的类

4.4.4、把非线程安全的集合转化为线程安全的集合
ArrayList是非线程安全的,换句话说,多个线程可以同时进入一个ArrayList对象的add方法

借助Collections.synchronizedLsit,可以把ArrayList转换为线程安全的List

与此类似的,还有HashSet,LinkedList,HashMap等等非线程安全的类,都通过工具类Collections转换为线程安全的

package multiplethread;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TestThread {
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = Collections.synchronizedList(list1);
}
}

4.5、线程之间的交互 wait和notify
线程之间有交互通知的需求,考虑如下情况:
有两个线程,处理同一个英雄。
一个加血,一个减血。

减血的线程,发现血量=1,就停止减血,直到加血的线程为英雄加了血,才可以继续减血

4.5.1、使用wait和notify进行线程交互
在Hero类中:hurt()减血方法:当hp=1的时候,执行this.wait().
this.wait() 表示 让占有this的线程等待,并临时释放占有
进入hurt方法的线程必然是减血线程,this.wait()会让减血线程临时释放对this的占有。 这样加血线程,就有机会进入recover()加血方法了。


recover() 加血方法:增加了血量,执行this.notify();
this.notify() 表示通知那些 等待在this的线程,可以苏醒过来了。 等待在this的线程,恰恰就是减血线程。 一旦recover()结束, 加血线程释放了this,减血线程,就可以重新占有this,并执行后面的减血工作。

4.5.2、关于wait、notify和notifyAll
留意wait()和notify()这两个方法是什么对象上的?

public synchronized void hurt() {
。。。
this.wait();
。。。
}

public synchronized void recover() {
。。。
this.notify();
}

这里需要强调的是,wait方法和notify方法,并 不是Thread线程上的方法,它们是Object上的方法。

因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。

wait()的意思是: 让占用了这个同步对象的 线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。

notify() 的意思是,通知 一个等待在这个同步对象上的线程, 可以苏醒过来了,有机会重新占用当前对象了。

notifyAll() 的意思是,通知 所有的等待在这个同步对象上的线程, 你们可以苏醒过来了,有机会重新占用当前对象了。

4.6、线程池
每一个线程的启动和结束都是比较消耗时间和占用资源的。

如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。

为了解决这个问题,引入线程池这种设计思想。

线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的 任务

4.6.1、线程池的设计思路
线程池的思路和生产者消费者是很接近的。
1. 准备一个任务容器
2. 一次性启动10个 消费者线程
3. 刚开始任务容器是空的,所以线程都 wait在上面。
4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被 唤醒notify
5. 这个消费者线程取出“任务”,并且 执行这个任务,执行完毕后,继续等待下一次任务的到来。
6. 如果短时间内,有较多的任务加入,那么就会有多个线程被 唤醒,去执行这些任务。

在整个过程中,都不需要创建新的线程,而是 循环使用这些已经存在的线程

4.6.2、开发一个自定义线程

线程池类:
package multiplethread;
import java.util.LinkedList;
public class ThreadPool {
// 线程池大小
int threadPoolSize;
// 任务容器
LinkedList<Runnable> tasks = new LinkedList<Runnable>();
// 试图消费任务的线程
public ThreadPool() {
threadPoolSize = 10;
// 启动10个任务消费者线程
synchronized (tasks) {
for (int i = 0; i < threadPoolSize; i++) {
new TaskConsumeThread("任务消费者线程 " + i).start();
}
}
}
public void add(Runnable r) {
synchronized (tasks) {
tasks.add(r);
// 唤醒等待的任务消费者线程
tasks.notifyAll();
}
}
class TaskConsumeThread extends Thread {
public TaskConsumeThread(String name) {
super(name);
}
Runnable task;
public void run() {
System.out.println("启动: " + this.getName());
while (true) {
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
task = tasks.removeLast();
// 允许添加任务的线程可以继续添加任务
tasks.notifyAll();
}
System.out.println(this.getName() + " 获取到任务,并执行");
task.run();
}
}
}
}

线程池调用类:
package multiplethread;
public class TestThread {
public static void main(String[] args) {
ThreadPool pool = new ThreadPool();
for (int i = 0; i < 5; i++) {
Runnable task = new Runnable() {
@Override
public void run() {
//System.out.println("执行任务");
//任务可能是打印一句话
//可能是访问文件
//可能是做排序
}
};
pool.add(task);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

4.6.3、使用Java自带的线程池
java提供自带的线程池,而不需要自己去开发一个自定义线程池了。

线程池类 ThreadPoolExecutor在包 java.util.concurrent

ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());


第一个参数10 表示这个线程池 初始化了10个线程在里面工作
第二个参数15 表示如果10个线程不够用了,就会自动增加到 最多15个线程
第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过 60秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就10个
第四个参数TimeUnit.SECONDS 如上
第五个参数 new LinkedBlockingQueue() 用来放任务的集合

execute方法用于添加新的任务

package multiplethread;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThread {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPool= new ThreadPoolExecutor(10, 15, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
threadPool.execute(new Runnable(){
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("任务1");
}
});
}
}

4.7、Lock对象
与synchronized类似,Lock也能达到同步的效果

4.7.1、使用Lock对象实现同步效果
Lock是一个接口,为了使用一个Lock对象,需要用到

Lock lock=new ReentrantLock();

synchronized (someObject) 类似的, lock()方法,表示当前线程占用lock对象,一旦占用,其他线程就不能占用了。
synchronized 不同的是,一旦synchronized 块结束,就会自动释放对 someObject的占用。 lock却必须调用 unlock方法进行手动释放,为了保证释放的执行,往往会把unlock() 放在finally中进行。

4.7.2、trylock方法
synchronized 是 不占用到手不罢休的,会一直试图占用下去。
与 synchronized 的 钻牛角尖不一样,Lock接口还提供了一个trylock方法。
trylock会在指定时间范围内 试图占用,占成功了,就啪啪啪。 如果时间到了,还占用不成功,扭头就走~

注意: 因为使用trylock有可能成功,有可能失败,所以后面unlock释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock,就会抛出异常

4.7.3、线程交互
使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法

Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的: await, signal,signalAll 方法

注意: 不是Condition对象的wait,nofity,notifyAll方法,是await,signal,signalAll
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

4.7.4、总结Lock和synchronized的区别
1、Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,Lock是代码层面的实现

2、Lock可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized不行,会一根筋一直获取下去。借助Lock这个特性,就能规避死锁,synchronized不惜通过谨慎和良好的设计,才能减少死锁的发生。

3、synchronized在发生异常和同步快结束时候,会自动释放锁。而Lock必须手动释放,所以如果放机释放锁,一样会造成死锁。

转载于:https://www.cnblogs.com/haxianhe/p/9271008.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值