线程学习
一 线程基础学习
什么是进程?
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础
如下图所示,一个PID对应一个进程。
什么是线程?
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
如下图所示,一个进程可能由一个或多个线程来执行
ps: 由此我们可以将进程可以看作一次系统进行资源分配和调度的战役,而线程就是其中大大小小作战的士兵,实际操作的单元
进程和线程的区别
- 一个程序至少有一个进程,一个进程至少有一个线程.
- 线程的划分尺度小于进程,使得多线程程序的并发性高。
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
- 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
Java实现线程的三种方式
- 继承Thread
- 实现Runnable接口(初中阶常用)
- 实现Callable(高阶必备)
1 继承Thread的方式
其中必须重写run方法,自定义线程执行逻辑。然后使用main方法new Thread(自定义线程类,线程名),调用run方法则只有主线程,调用start则启用主副线程模式。
ps:此方法适合初学,使用一个则必须继承Thread,不够多态,不推荐使用
public class ThreadTest1 extends Thread{
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println("正在学习thread---->"+Thread.currentThread().getName()+i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 20000; i++) {
System.out.println("父线程执行代码------>"+Thread.currentThread().getName()+i);
}
new Thread(new ThreadTest1(),"子线程").run();//调用run 则只有一个mian线程
new Thread(new ThreadTest1(),"子线程").start();//调用start。则mian和子线程并行代码
}
}
2 实现Runnable接口方式
同上,重写run方法,线程所有处理都在run()中进行定义,线程启动是调用start()方法,才是真正启动线程,而启动start()方法时会自动调用run()方法
public class ThreadTest2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("正在学习thread---->"+Thread.currentThread().getName()+i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 20000; i++) {
System.out.println("父线程执行代码------>"+Thread.currentThread().getName()+i);
}
new Thread(new ThreadTest2(),"子线程").run();
//new Thread(new ThreadTest2(),"子线程").start();
}
}
ps:其中run()和start()方法区别:如下图所示
由图可知,使用run()方法类似于javaScript一样,只有一个执行路径,而start则主副交替执行
3 实现Callable接口方式
实现Callable<线程返回参数值类型>接口,编写call(),自定义线程执行逻辑,使用FutureTask作为对象使用
new Thread(FutureTask).run() / new Thread(FutureTask).start()
public class ThreadTest3 implements Callable<Integer> {
public Integer call() throws Exception {
int i=0;
for (; i < 200; i++) {
System.out.println(Thread.currentThread().getName()+"正在看第"+i+"本书");
}
return i;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadTest3 t3=new ThreadTest3();
FutureTask<Integer> futureTask=new FutureTask<Integer>(t3);
new Thread(futureTask).start();
System.out.println("当前线程返回值"+futureTask.get());
}
}
**高阶写法 lambda表达式 **
public class ThreadLambda{
public static void main(String[] args) {
new Thread(()-> System.out.println("学习thread")).start();
}
}
线程锁的学习
什么是锁?锁顾名思义就是锁住东西
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。
这样说可能有点空洞,那么我们提到锁,就必须提到多线程的线程泄露:当多个线程访问一个数据对象时,容易对数据造成的破坏,举个经典的抢票实例
public class ThreadSafe implements Runnable {
private int count = 10;
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 售票方法,解耦合
*/
public void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + ",拿到了出售的第" + (10 - count + 1) + "张票");
count--;
}
}
public static void main(String[] args) {
ThreadSafe t1 = new ThreadSafe();
new Thread(t1, "小杨").start();
new Thread(t1, "小张").start();
}
}
如图所示,这里使用Thread.sleep(100);是为了让扩大因素,让线程更容易出险问题。我们初始化6张票,然后让小张和小阳连个线程去抢票(start方式),按照正常逻辑,在线程优先级一致(线程优先级并不能完全左右线程执行顺序,下文会详细介绍),不会出现抢到同一张票
如图所示,小张和小杨多次抢到同一张票,小张和小杨按照短信提示消息来找老板要票,老板都傻了,为了避免这种情况,我们必须线程同步,在店铺前加一个大门,大门上有一个锁,只有有钥匙的人,我们才允许他进来买票,没钥匙的人就在外面等着
锁的两种实现方式
使用锁的必备前提:
- 存在共享数据
- 多线程共同操作共享数据
按照抢票列子,共享数据(票数),而多线程共同操作共享数据(小张和小杨抢占票数),这种情况我们必须使用锁,所以以后我们使用锁,首先分析代码中是否存在共享数据,然后看是否有多个线程使用这个数据,
ps: 不要动不动就锁this对象,影响性能
-
synchronized 隐式锁
java任何一个对象都可以作为锁,synchronized可以作用于方法和变量上,同步方法和同步代码块都可以,这里我们只要用synchronized锁住票数更改这块代码块就行
先在在这个ThreadSafe类下定义一个变量
private static Object object = new Object();
然后使用
public void sale() { synchronized(object){ if (count > 0) { System.out.println(Thread.currentThread().getName() + ",拿到了出售的第" + (6 - count + 1) + "张票"); count--; } } }
或者使用在sale()方法上修饰synchronized效果一致
通过synchronized锁住关键的对象object,此时代码跑起来,完全没问题
- lock 显式锁
lock 也是锁的一种写法,使用jdk 1.5的ReentrantLock对象,使用锁lock.lock(),释放锁lock.unlock();
public ReentrantLock lock=new ReentrantLock();
为什么称synchronized为隐式锁,lock为显式锁?
因为使用synchronized,只用关注在哪里使用锁,不用关心锁的释放,由系统自动判断
使用lock,在哪里用锁,哪里释放锁都可以自己控制,十分只管,所以称为显示
public class TestLock {
public static void main(String[] args) {
ThreadLock t1=new ThreadLock();
new Thread(t1,"小杨").start();
new Thread(t1,"小张").start();
}
}
class ThreadLock implements Runnable{
public ReentrantLock lock=new ReentrantLock(); //使用jdk 5以后的显式锁 ReentrantLock
public int ticketNum=6;
@Override
public void run() {
while(true){
try{
lock.lock();
if(ticketNum>0){
System.out.println(Thread.currentThread().getName()+":买到了第"+(7-ticketNum--)+"票");
}else{
break;
}
Thread.sleep(100);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
效果如下:
什么是死锁
dead lock:是开发者多线程编程常见问题,是根本原因在于不了解何为锁,如何锁,如何释放,是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
如图所示:线程A使用锁占用着资源2,释放锁后就要获取资源1。而线程B使用占用资源1,释放锁就要获取资源2。但是谁也没有释放,最终就会形成死循环,俗称死锁
//线程1
Synchorized(objectA){
Synchorized(objectB){
//操作
}
}
//线程2
Synchorized(objectB){
Synchorized(objectA){
//操作
}
}