Java线程
进程与线程:
进程是系统一个程序执行时的一个实例,系统为其分配资源。进程之间相互独立。每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;如果一个进程想要访问另一个进程的资源,需要使用进程之间的通信方式,比如管道,套接字等方法。
每个应用打开后都会创建进程,占用系统资源。
一个程序运行后至少有一个进程,一个进程中可以包含多个线程
线程是进程的一个实体,是进程的一条执行路线。一个进程可以有多个线程,同一个进程内的多个线程可以共享资源。
进程的麻烦:
如果进程需要频繁创建销毁,耗费资源较大,效率更低,使用线程可以提高应用程序响应速度。
java创建线程类
一:继承Thread类
public class FirstThread extends Thread {
private String name;
//构造方法,创建名字为name线程
public FirstThread(String name){
super(name);
this.name = name;
}
@Override
public void run(){
for (int i = 1; i <= 5; i++) {
System.out.println("第"+i+"次执行"+name+"线程");
}
}
}
需要重写run方法,完成该线程的执行逻辑代码
创建第二个线程类
public class SecondThread extends Thread {
private String name;
public SecondThread(String name){
super(name);
this.name = name;
}
@Override
public void run(){
for (int i = 1; i <= 5; i++) {
System.out.println("第"+i+"次执行"+name+"线程");
}
}
}
测试
public class Test {
public static void main(String[] args) {
FirstThread first = new FirstThread("first");
SecondThread second = new SecondThread("second");
//开启线程
first.start();
second.start();
}
}
会得到如下打印结果
第1次执行first线程
第1次执行second线程
第2次执行first线程
第2次执行second线程
第3次执行first线程
第3次执行second线程
第4次执行first线程
第4次执行second线程
第5次执行first线程
第5次执行second线程
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
Thread类
构造方法:
public Thread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
Runable接口是创建线程的另一种实现方式
常用方法:
public String getName() :获取当前线程名称。
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run() :此线程要执行的任务在此处定义代码。
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
二:实现Runable接口
步骤:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
的线程对象。 - 调用线程对象的start()方法来启动线程。
public class FirstRunable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("第"+i+"次执行"+Thread.currentThread().getName()+"线程");
}
}
}
public class SecondRunable implements Runnable {
@Override
public void run(){
for (int i = 1; i <= 5; i++) {
System.out.println("第"+i+"次执行"+Thread.currentThread().getName()+"线程");
}
}
}
public class Test {
public static void main(String[] args) {
FirstRunable first = new FirstRunable();
SecondRunable second = new SecondRunable();
Thread firstRunable = new Thread(first,"firstRunable");
Thread secondRunable = new Thread(second,"secondRunable");
firstRunable.start();
secondRunable.start();
}
}
第1次执行firstRunable线程
第1次执行secondRunable线程
第2次执行firstRunable线程
第2次执行secondRunable线程
第3次执行firstRunable线程
第3次执行secondRunable线程
第4次执行firstRunable线程
第4次执行secondRunable线程
第5次执行firstRunable线程
第5次执行secondRunable线程
Thread和Runable方式区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
线程安全问题
多个线程对同一个资源进行增删改操作时,导致该资源与预期结果不一致,则称为线程不安全。
问题说明:
桌子上有10个蛋糕,多个小孩同时去取蛋糕。
使用Runable接口实现类来模拟蛋糕,多个线程模拟小孩
public class CakeRunable implements Runnable {
private int cake = 10;
@Override
public void run() {
while(true){
//有蛋糕就去取
if(cake>0){
try {
//模拟取蛋糕的时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"取了第"+(11-cake)+"个蛋糕");
cake--;
}
}
}
}
public class Test {
public static void main(String[] args) {
CakeRunable cakeRunable = new CakeRunable();
Thread t1 = new Thread(cakeRunable,"张三");
Thread t2 = new Thread(cakeRunable,"李四");
Thread t3 = new Thread(cakeRunable,"傻蛋");
t1.start();
t2.start();
t3.start();
}
}
傻蛋取了第1个蛋糕
李四取了第1个蛋糕
张三取了第1个蛋糕
傻蛋取了第4个蛋糕
李四取了第4个蛋糕
张三取了第6个蛋糕
李四取了第7个蛋糕
傻蛋取了第7个蛋糕
张三取了第9个蛋糕
李四取了第10个蛋糕
傻蛋取了第10个蛋糕
张三取了第12个蛋糕
可以看到结果十分错误,这是因为多个线程共享资源时出现了线程不安全问题
线程同步
解决线程不安全问题
当有小孩去取蛋糕时,其他小孩不能去取蛋糕,必须等待他取完蛋糕
也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
方式:
- 同步代码块。
- 同步方法。
- 锁机制。
一:同步代码块
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){
需要同步操作的代码
}
同步锁::可以是任意类型,谁拿到锁才拥有访问资源的权限,其他线程只能等待
p
ublic class CakeRunable implements Runnable {
private int cake = 10;
Object lock = new Object();
@Override
public void run() {
while(true) {
//使用同步代码块
synchronized (lock) {
//有蛋糕就去取
if (cake > 0) {
try {
//模拟取蛋糕的时间
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "取了第" + (11 - cake) + "个蛋糕");
cake--;
}
}
}
}
}
张三取了第1个蛋糕
张三取了第2个蛋糕
傻蛋取了第3个蛋糕
李四取了第4个蛋糕
李四取了第5个蛋糕
李四取了第6个蛋糕
李四取了第7个蛋糕
李四取了第8个蛋糕
李四取了第9个蛋糕
李四取了第10个蛋糕
可以看到蛋糕的数量没有出错,这就达到线程安全
二:同步方法
public class CakeRunable implements Runnable {
private int cake = 10;
@Override
public void run(){
while(true){
takeCake();
}
}
public synchronized void takeCake(){
//有蛋糕就去取
if (cake > 0) {
try {
//模拟取蛋糕的时间
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "取了第" + (11 - cake) + "个蛋糕");
cake--;
}
}
}
张三取了第1个蛋糕
傻蛋取了第2个蛋糕
傻蛋取了第3个蛋糕
李四取了第4个蛋糕
傻蛋取了第5个蛋糕
傻蛋取了第6个蛋糕
傻蛋取了第7个蛋糕
傻蛋取了第8个蛋糕
张三取了第9个蛋糕
傻蛋取了第10个蛋糕
三:锁机制
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
public class CakeRunable implements Runnable{
private int cake = 10;
Lock lock = new ReentrantLock();
@Override
public void run(){
while(true){
lock.lock();
if(cake>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"取了第"+(11-cake--)+"个蛋糕");
}
lock.unlock();
}
}
}
张三取了第1个蛋糕
张三取了第2个蛋糕
张三取了第3个蛋糕
张三取了第4个蛋糕
张三取了第5个蛋糕
傻蛋取了第6个蛋糕
李四取了第7个蛋糕
李四取了第8个蛋糕
李四取了第9个蛋糕
李四取了第10个蛋糕
可以看到即使通过不同方法达到线程安全后,但是多个线程获得资源是随机的,
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制
在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 **notifyAll()**来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
-
wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时
的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象
上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中 -
notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先
入座。 -
notifyAll:则释放所通知对象的 wait set 上的全部线程。
调用wait和notify方法需要注意的细节
-
wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
-
wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
-
wait方法与notify方法必须要在同步代码块或者是同步方法中使用。因为:必须要通过锁对象调用这2个方法。
线程池
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作无需反复创建线程而消耗过多资源。
Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
public class Test {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(3);
CakeRunable cakeRunable = new CakeRunable();
es.submit(cakeRunable);
es.submit(cakeRunable);
es.submit(cakeRunable);
}
}