线程与进程的概念
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
java里面实现多线程编程的方法有三个:
1:继承Thread类
2.实现Runable接口
3.实现Callable接口,并且与Future,和线程池相结合使用
入门学习参考
注意
main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
继承Thread类和Runable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
继承Runnable接口的的类的对象声明时用这种办法是错误的
Thread myThread02=new Thread(new MyThread02(“C”));
myThraed02根本就没有.start()方法无法运行;
我的理解:
参考Thread类的源码参考博客
里面有run(),init(),jion()等办法;第一种方式继承Thread类时可以重新定义这些办法,也可以不重新写这些办法,但是继承Runnable接口一定要重写run()办法;但是Runable没有start()办法,是不是自己写的线程类就无法启动???就要用Thread(Runnable target)这种构造器来初始化线程
/*Thread thread = new Thread(new MyThread02(“C”));
Thread thread01= new Thread(new MyThread02(“D”));
Thread.start()报错
*/
new Thread(new MyThread02(“C”)).start();//正确
new Thread(new MyThread02(“D”)).start();
Thread的其他构造器见上源码;由此可见实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
状态转化图
线程数据的传递的3种方式
1.通过构造器传递:
如:new MyThread01(Object a,Object b,Object c )
对象a,b,c就是传递给这个新建线程的数据;
2.通过 自定义线程中的属性或者方法传递数据
比如,MyThread02这类里面有name, id ,…等属性,可通过这些属性的set方法来传入数据到这个线程里面,myThread02.setName(“线程01”);
3:通过回调函数来传递
就是自定义线程类里面有其他的类作为自己的属性,再通过这个属性类的构造器,set方法来传入参数,实际就是传到了这个线程里面
java关键字synchronized()同步
Synchronized和Lock区别
随着学习的进行我们知道在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它。
不过,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。下面来一起探索synchronized的基本使用、实现机制、Java是如何对它进行了优化、锁优化机制、锁的存储结构等升级过程。
1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。
Synchronized三种用法
1.作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
2.作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
3.作用于代码块,这需要指定加锁的对象,对所给的指定对象加锁,进入同步代码前要获得指定对象的锁。
具体代码参考
java中Lock类的使用
Jdk1.5以后,在java.util.concurrent.locks包下,有一组实现线程同步的接口和类,说到线程的同步,可能大家都会想到synchronized关键字,
这是java内置的关键字,用来处理线程同步的,但这个关键字有很多的缺陷,使用起来也不是很方便和直观,所以就出现了Lock,下面,我们
就来对比着讲解Lock。
通常我们在使用synchronized关键字的时候会遇到下面这些问题:
(1)不可控性,无法做到随心的加锁和释放锁。
(2)效率比较低下,比如我们现在并发的读两个文件,读与读之间是互不影响的,但如果给这个读的对象使用synchronized来实现同步的话,
那么只要有一个线程进入了,那么其他的线程都要等待。
(3)无法知道线程是否获取到了锁。
而上面synchronized的这些问题,Lock都可以很好的解决,并且jdk1.5以后,还提供了各种锁,例如读写锁,但有一点需要注意,使用synchronized
关键时,无须手动释放锁,但使用Lock必须手动释放锁。下面我们就来学习一下Lock锁。
Lock是一个上层的接口,其原型如下,总共提供了6个方法:
public interface Lock {
// 用来获取锁,如果锁已经被其他线程获取,则一直等待,直到获取到锁
void lock();
// 该方法获取锁时,可以响应中断,比如现在有两个线程,一个已经获取到了锁,另一个线程调用这个方法正在等待锁,但是此刻又不想让这个线程一直在这死等,可以通过
调用线程的Thread.interrupted()方法,来中断线程的等待过程
void lockInterruptibly() throws InterruptedException;
// tryLock方法会返回bool值,该方法会尝试着获取锁,如果获取到锁,就返回true,如果没有获取到锁,就返回false,但是该方法会立刻返回,而不会一直等待
boolean tryLock();
// 这个方法和上面的tryLock差不多是一样的,只是会尝试指定的时间,如果在指定的时间内拿到了锁,则会返回true,如果在指定的时间内没有拿到锁,则会返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 实现线程通信,相当于wait和notify,后面会单独讲解
Condition newCondition();
}
那么这几个方法该如何使用了?前面我们说到,使用Lock是需要手动释放锁的,但是如果程序中抛出了异常,那么就无法做到释放锁,有可能引起死锁,
所以我们在使用Lock的时候,有一种固定的格式,如下:
Lock l = …;
l.lock();
try {
// access the resource protected by this lock
} finally {// 必须使用try,最后在finally里面释放锁
l.unlock();
}
下面我们来看一个简单的例子,代码如下:
/**
-
描述:Lock使用
*/
public class LockDemo {
// new一个锁对象,注意此处必须声明成类对象,保持只有一把锁,ReentrantLock是Lock的唯一实现类
Lock lock = new ReentrantLock();
public void readFile(String fileMessage){
lock.lock();// 上锁
try{
System.out.println(Thread.currentThread().getName()+“得到了锁,正在读取文件……”);
for(int i=0; i<fileMessage.length(); i++){
System.out.print(fileMessage.charAt(i));
}
System.out.println();
System.out.println(“文件读取完毕!”);
}finally{
System.out.println(Thread.currentThread().getName()+“释放了锁!”);
lock.unlock();
}
}public void demo(final String fileMessage){
// 创建若干个线程
ExecutorService service = Executors.newCachedThreadPool();
// 提交20个任务
for(int i=0; i<20; i++){
service.execute(new Runnable() {
@Override
public void run() {
readFile(fileMessage);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 释放线程池中的线程
service.shutdown();
}
}
线程池
corePoolSize :线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程。
当有任务过来的时候才会去创建创建线程执行任务。换个说法,线程池创建之后,线程池中的线程数为0,当任务过来就会创建一个线程去执行,直到线程数达到corePoolSize 之后,就会被到达的任务放在队列中。(注意是到达的任务)。换句更精炼的话:corePoolSize 表示允许线程池中允许同时运行的最大线程数。
线程池的作用:
减少资源的开销
减少了每次创建线程、销毁线程的开销。
提高响应速度 ,每次请求到来时,由于线程的创建已经完成,故可以直接执行任务,因此提高了响应速度。
提高线程的可管理性 ,线程是一种稀缺资源,若不加以限制,不仅会占用大量资源,而且会影响系统的稳定性。 因此,线程池可以对线程的创建与停止、线程数量等等因素加以控制,使得线程在一种可控的范围内运行,不仅能保证系统稳定运行,而且方便性能调优。
执行流程:
ThreadPoolExecutor是线程池的真正实现,通过构造方法的一系列参数,来构成不同配置的线程池。
在HotSpot虚拟机中,Java中的线程将会被一一映射为操作系统的线程 在Java虚拟机层面,用户将多个任务提交给Executor框架,Executor负责分配线程执行它们; 在操作系统层面,操作系统再将这些线程分配给处理器执行。。
Executor框架中的所有类可以分成三类:
任务
任务有两种类型:Runnable和Callable。
任务执行器
Executor框架最核心的接口是Executor,它表示任务的执行器。
Executor的子接口为ExecutorService。
ExecutorService有两大实现类:ThreadPoolExecutor和ScheduledThreadPoolExecutor。
执行结果
Future接口表示异步的执行结果,它的实现类为FutureTask。
参考文献