Java基础之线程

线程的概念

进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等。线程是进程中的一个实体,是CPU调度和分配的基本单位,它是比进程更小的、能独立运行的基本单位。同属一个进程的所有线程共享进程的全部资源。一个进程至少需要有一个线程,即主线程。

CPU核数和线程数的关系

目前主流CPU都是多核的,增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下他们是1:1 对应关系。但是Intel引入超线程技术后,核心数与线程数形成1:2的关系。

CPU时间片轮转机制

平时开发过程中开启线程并没有感受到cpu核心数的限制,想开几个线程就开几个线程,这正是因为操作系统提供了一种叫CPU时间片轮转机制。时间片是一个进程可使用的时间段。

CPU时间片轮转机制的原理是:如果在时间片结束时进程还在运行,则CPU 将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU 当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。进程之间切换(也成为“上下文切换)是需要有时间开销的。

所以,时间片设得太短会导致过多的进程切换,降低了CPU 效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms 通常是一个比较合理的折中。

并行和并发

并发:单位时间内应用能够交替执行不同的任务,比如单CPU 核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,以达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已。

并行:同一时间内应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行。

两者区别:一个是交替执行,一个是同时执行。

高并发的好处
  • 充分利用CPU 的资源
  • 加快响应用户的时间
  • 可以使你的代码模块化,异步化,简单化
多线程程序注意事项
  • 线程之间的安全性
  • 线程之间的死锁
  • 线程太多了会将服务器资源耗尽形成死机当机

开启线程的方式

1、X extends Thread;,然后X.start
2、X implements Runnable;然后交给Thread 运行

/**
 *类说明:新启线程的方式
 */
public class NewThread {
	/*扩展自Thread类*/
	private static class UseThread extends Thread{
		@Override
		public void run() {
			super.run();
			// do my work;
			System.out.println("I am extendec Thread");
		}
	}
	/*实现Runnable接口*/
	private static class UseRunnable implements Runnable{

		@Override
		public void run() {
			// do my work;
			System.out.println("I am implements Runnable");
		}
	}
	public static void main(String[] args) 
			throws InterruptedException, ExecutionException {
		UseThread useThread = new UseThread();
		useThread.start();
		//调多次start()会抛异常
		//useThread.start();

		UseRunnable useRunnable = new UseRunnable();
		new Thread(useRunnable).start();
	}
}

Thread 和Runnable 的区别

Thread 才是Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑)的抽象。Thread 可以接受任意一个Runnable 的实例并执行。

深入理解run()和start()

Thread 类是Java 里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new 出一个Thread 的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。start()方法让一个线程进入就绪队列等待分配cpu,分到cpu 后才调用实现run()方法。

start()方法不能重复调用,如果重复调用会抛出异常。

run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方
法并没有任何区别,可以重复执行,也可以被单独调用。

停止线程的方式

中止

线程自然中止

要么是run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

suspend()、resume()和stop()

暂停、恢复和停止操作对应在线程Thread 的API 就是suspend()、resume()和stop()。但是这些API 是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会就直接停止,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

中断

安全的中止则是其他线程通过调用某个线程A 的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A 会立即停止自己的工作,同样的A 线程完全可以不理会这种中断请求。因为java 里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true 来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait 等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

不建议自定义一个取消标志位来中止线程的运行。因为run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为:
一、一般的阻塞方法,如sleep 等本身就支持中断的检查,
二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

线程常用方法和线程的状态

线程安全问题

锁的类型:

synchronized

修饰方法或者以同步块的形式来进行使用,锁一个对象,确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

/**
 *类说明:synchronized关键字的使用方法
 */
public class SynTest {

	private long count =0;
	private Object obj = new Object();//作为一个锁

	public long getCount() {
		return count;
	}

	public void setCount(long count) {
		this.count = count;
	}

	/*用在同步块上*/
	public void incCount(){
		synchronized (obj){
			count++;
		}
	}
	
	/*用在方法上*/
	public synchronized void incCount2(){
			count++;
	}
	
	/*用在同步块上,但是锁的是当前类的对象实例*/
	public void incCount3(){
		synchronized (this){
			count++;
		}
	}

	//线程
	private static class Count extends Thread{

		private SynTest simplOper;

		public Count(SynTest simplOper) {
			this.simplOper = simplOper;
		}

		@Override
		public void run() {
			for(int i=0;i<10000;i++){
				simplOper.incCount2();//count = count+10000
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		SynTest simplOper = new SynTest();
		//启动两个线程
		Count count1 = new Count(simplOper);
		Count count2 = new Count(simplOper);
		Count count3 = new Count(simplOper);
		count1.start();
		count2.start();
		count3.start();
		Thread.sleep(50);
		System.out.println(simplOper.count);//20000
	}
}

volatile,最轻量的同步机制

volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
volatile 保证可见性、有序行、不保证原子性。

volatile 最适用的场景:一个线程写,多个线程读。

不保证原子性的原理如下图所示i++的操作:
在这里插入图片描述

ThreadLocal

ThreadLocal 和Synchonized 都用于解决多线程并发訪问。可是ThreadLocal与synchronized 有本质的差别。synchronized 是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。而ThreadLocal 为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

ThreadLocal 的使用

ThreadLocal 类接口很简单,只有4 个方法,我们先来了解一下:
• void set(Object value)
设置当前线程的线程局部变量的值。

• public Object get()
该方法返回当前线程所对应的线程局部变量。

• public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

• protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected 的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1 次调用get()或set(Object)时才执行,并且仅执行1 次。ThreadLocal 中的缺省实现直接返回一个null。

/**
 *类说明:演示ThreadLocal的使用
 */
public class UseThreadLocal {
	
	private static ThreadLocal<Integer> intLocal
            = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };
    /**
     * 运行3个线程
     */
    public void StartThreadArray(){
        Thread[] runs = new Thread[3];
        for(int i=0;i<runs.length;i++){
            runs[i]=new Thread(new TestThread(i));
        }
        for(int i=0;i<runs.length;i++){
            runs[i].start();
        }
    }
    
    /**
     *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
     */
    public static class TestThread implements Runnable{
        int id;
        public TestThread(int id){
            this.id = id;
        }
        public void run() {
            System.out.println(Thread.currentThread().getName()+":start");
            Integer s = intLocal.get();
            s = s+id;
            intLocal.set(s);
            System.out.println(Thread.currentThread().getName()
                    +":"+ intLocal.get());
            //intLocal.remove();
        }
    }

    public static void main(String[] args){
    	UseThreadLocal test = new UseThreadLocal();
        test.StartThreadArray();
    }
}

线程更多相关知识

在这里插入图片描述

notify()

通知一个在对象上等待的线程,使其从wait 方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING 状态,只能唤醒一个线程。

notifyAll()

通知所有等待在该对象上的所有线程。

wait()

调用该方法的线程进入WAITING 状态,只有等待另外线程的通知(notify或nitifyall)或被中断才会返回。需要注意,调用wait()方法后,会释放对象的锁。

wait(long)

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n 毫秒,如果没有通知就超时返回。

wait (long,int)

对于超时时间更细粒度的控制,可以达到纳秒。

只能在同步方法或同步块中调用wait()方法、notify()系列方法

yield()

将线程从运行状态转到就绪状态,让出时间片,但是不会释放锁。

join

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B 中调用了线程A 的Join()方法,直到线程A 执行完毕后,才会继续执行线程B。

setDaemon()

设置成守护线程,当主线程结束时,该线程也跟着一起结束。

sleep()

线程进入阻塞状态,但是不会释放锁。

interrupt()

中断线程,只是改变线程的标志位,提醒线程中断,是否中断由线程自己决定。

setPriority()

设置优先级,不一定会起作用,取决于操作系统。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值