精华内容
下载资源
问答
  • delphiXE关于线程和多线程、线程的同步与异步执行
    千次阅读
    2020-04-23 02:07:55

    delphiXE关于线程和多线程、线程的同步与异步执行

    一、最好的参照案例

    {$BDS}\source\fmx\FMX.Helpers.Android.pas

    如下四个独立方法:

    type
      TMethodCallback = procedure of object;
      TCallBack = reference to procedure;

    procedure CallInUIThread(AMethod: TMethodCallback); overload;  //:在UI中调用线程并回调某个对象或类的事件:在自己的单元文件重新实现
    procedure CallInUIThread(AMethod: TCallBack); overload; //:在UI中调用线程并回调某个匿名过程:在自己的单元文件重新实现
    procedure CallInUIThreadAndWaitFinishing(AMethod: TMethodCallback); overload; //:在UI中调用线程且等待其结束并回调某个对象或类的事件:在自己的单元文件重新实现
    procedure CallInUIThreadAndWaitFinishing(AMethod: TCallBack); overload; //:在UI中调用线程且等待其结束并回调某个匿名过程:在自己的单元文件重新实现

    implementation

    use FMX.Types, System.Generics.Collections,
      System.SyncObjs, System.Rtti, System.Classes, FMX.Consts;

    var
        //ActiveJavaRunnables: TThreadList<TRunnable>;  //改为:
        ActiveThreadsRunnables: TThreadList<TRunnable>;  //:活动的能运行的线程列表:uses System.Generics.Collections


    initialization
      ActiveJavaRunnables := TThreadList<TRunnable>.Create;

    finalization
      ActiveJavaRunnables.DisposeOf;
    end.

    二、将其稍作修改,变成你自己的通用线程执行类

    三、方法:

    1、简单的线程同步:

    1.1、当前线程,简单的线程同步

        try
          //在这里写i的数据获取及处理:......
        finally
          TThread.Synchronize(nil,
            // var :你的同步中间变量;   
          procedure begin
              Memo1.Lines.Add( LResp.ContentAsString( System.SysUtils.TEncoding.UTF8) );
                //LResp.ContentAsString(System.SysUtils.TEncoding.UTF8);
                //Memo1.Lines.Add(LRespoContent.DataString); //:乱码转换下 ::这样也可以
          end );  //:与UI交互,信息量少,可以这样简单短暂地阻塞一下UI;
              //:若信息量大,请用异步方式处理完数据,得到通知后再加载UI

          或用队列同步:    

          TThread.Queue(nil,
          // var :你的队列中间变量;   
          procedure begin
              //在这里写您与UI界面元素的交互代码:
          end;

    1.2、当前线程的匿名线程,简单的线程同步

          TThread.CreateAnonymousThread (
          procedure
              // var :你的匿名线程中间变量; 
          begin
              //在这里写您与UI界面元素的交互代码:
          end;

    1.3、当前线程的子线程,简单的线程同步

          var LSubAnonymousThread :TAnonymousThread;

          LSubAnonymousThread := TAnonymousThread.Create(
          procedure
              // var :你的匿名线程中间变量; 
          begin
              //在这里写您与UI界面元素的交互代码:
          end;

    2、其它,见本博客相关文章:

        2.1、Delphi中的匿名方法及异步任务:

            https://blog.csdn.net/pulledup/article/details/102675723

        2.2、PPL并行编程库01-概念

            https://blog.csdn.net/pulledup/article/details/102063074

        2.3、PPL并行编程库02-多任务

           https://blog.csdn.net/pulledup/article/details/102068352

        2.4、PPL并行编程库03-单任务、异步多任务、同步多任务

            https://blog.csdn.net/pulledup/article/details/102081210

        2.5、delphi线程监视器System.TMonitor  : 若存在线程嵌套 或 与UI交互时有时需要强制同步:

            https://blog.csdn.net/pulledup/article/details/105175193

        2.6、请求远程数据的同步及异步处理1:

            delphi Restful:客户端实现的四种方式及其比较:  https://blog.csdn.net/pulledup/article/details/104132753

        2.7、请求远程数据的同步及异步处理2: 

            案例:delphi判断网络状态是否正常:  https://blog.csdn.net/pulledup/article/details/104584564

        2.8、多线程同步对象及其等待结果

            https://blog.csdn.net/pulledup/article/details/106136991

        2.9、再谈delphi XE多线程同步对象及其管理二

            https://blog.csdn.net/pulledup/article/details/106631889

    喜欢的话,就在下面点个赞、收藏就好了,方便看下次的分享:

     

    更多相关内容
  • Spring Boot 多线程数据同步

    千次阅读 2020-08-08 17:05:39
    因为最近项目上线,需要同步期初数据-工序,大概有120万数据,采用之前Mybatis批量插入,一次5000的方式,单线程,大概需要近半个小时,后面为了提高效率,采用多线程编程,速度提升了大概三倍,耗时15分钟,同步120...

    因为最近项目上线,需要同步期初数据-工序,大概有120万数据,采用之前Mybatis批量插入,一次5000的方式,单线程,大概需要近半个小时,后面为了提高效率,采用多线程编程,速度提升了大概2倍,耗时15分钟,同步120万条数据数

    采用的是SpringBoot的多线程和@Async和Future

    先了解下概念:

    此处引用其他网站的解释:

    什么是SpringBoot多线程

    Spring是通过任务执行器(TaskExecutor)来实现多线程和并发编程,使用ThreadPoolTaskExecutor来创建一个基于线城池的TaskExecutor。在使用线程池的大多数情况下都是异步非阻塞的。我们配置注解@EnableAsync可以开启异步任务。然后在实际执行的方法上配置注解@Async上声明是异步任务

    以下写法是单线程一次新增5000数据

    public void syncWholeTools() {
    		// 获取最大的AutoID
    		Integer maxAutoId = readToolsService.getMaxAutoId();
    		final Integer batchNumber = 5000;
    		Integer count = maxAutoId / batchNumber;
    		Integer currentAotoId = 0;
    		//分批次新增
    		for (int i = 0; i < count; i++) {
    			List<ReadToolModel> readToolModelList = readToolsService.getToolListByAutoId(currentAotoId,
    					(i + 1) * batchNumber);
    			currentAotoId = (i + 1) * batchNumber + 1;
    			writeToolService.createBomDetail(readToolModelList);
    		}
    		List<ReadToolModel> readToolModelList = readToolsService.getToolListByAutoId(currentAotoId, maxAutoId);
    		writeToolService.createBomDetail(readToolModelList);
    	}

    使用Spring Boot多线程

    启动类需要加上@EnableAsync注解

    yml配置

    tools:
      core:
        poolsize: 100
      max:
        poolsize: 200
      queue:
        capacity: 200
      keepAlive:
        seconds: 30
      thread:
        name:
          prefix: tool

    配置类:

    
    @Configuration
    @EnableAsync
    public class AsyncConfig {
    
    	//接收报文核心线程数
    	@Value("${tools.core.poolsize}")
    	private int toolsCorePoolSize;
    	//接收报文最大线程数
    	@Value("${tools.max.poolsize}")
    	private int toolsMaxPoolSize;
    	//接收报文队列容量
    	@Value("${tools.queue.capacity}")
    	private int toolsQueueCapacity;
    	//接收报文线程活跃时间(秒)
    	@Value("${tools.keepAlive.seconds}")
    	private int toolsKeepAliveSeconds;
    	//接收报文默认线程名称
    	@Value("${tools.thread.name.prefix}")
    	private String toolsThreadNamePrefix;
    
    	/**
    	 * toolsTaskExecutor:(接口的线程池). <br/>
    	 *
    	 * @return TaskExecutor taskExecutor接口
    	 * @since JDK 1.8
    	 */
    	@Bean(name = "ToolsTask")
    	public ThreadPoolTaskExecutor toolsTaskExecutor() {
    		//newFixedThreadPool
    		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    		// 设置核心线程数
    		executor.setCorePoolSize(toolsCorePoolSize);
    		// 设置最大线程数
    		executor.setMaxPoolSize(toolsMaxPoolSize);
    		// 设置队列容量
    		executor.setQueueCapacity(toolsQueueCapacity);
    		// 设置线程活跃时间(秒)
    		executor.setKeepAliveSeconds(toolsKeepAliveSeconds);
    		// 设置默认线程名称
    		executor.setThreadNamePrefix(toolsThreadNamePrefix);
    		// 设置拒绝策略
    		// rejection-policy:当pool已经达到max size的时候,如何处理新任务
    		// CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
    		executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    		// 等待所有任务结束后再关闭线程池
    		executor.setWaitForTasksToCompleteOnShutdown(true);
    		executor.initialize();
    		return executor;
    	}
    }

     

    数据新增的操作

    @Component
    public class SyncToolsHandler {
        
        private static final Logger LOG = LoggerFactory.getLogger(SyncToolsHandler.class);
        
        @Autowired
        WriteToolService writeToolService;
        
        @Async(value = "ToolTask")
        public Future <String> syncTools(List <ReadToolModel> readToolList, int pageIndex) {
    
            System.out.println("thread name " + Thread.currentThread().getName());
            
            LOG.info(String.format("此批数据的段数为:%s 此段数据的数据条数为:%s", pageIndex, readToolList.size()));
            
            //声明future对象
            Future <String> result = new AsyncResult <String>("");
            
            //循环遍历
            if (null != readToolList && readToolList.size() > 0) {
                    try {
                    	int listSize = readToolList.size();
            			int listStart = 0, listEnd = 0;
            			int ropeNum = listSize/2000;
            			/**
            			 * 假设101条数据每次40,总共循环101/4=2
            			 * 0<=X<40     i=0  40*i   40*i+40
            			 * 40<=X<80   i=1  40*i   40*i+40
            			 * 80<=X<101  40*i+40   101
            			 */
            			for(int i = 0 ; i < ropeNum; i++) {
            				//数据入库操作
            				listStart = i *2000;
            				listEnd = i * 2000 +2000;
                            writeToolService.createBomDetail(readToolList.subList(listStart, listEnd));
            			}
            			writeToolService.createBomDetail(readToolList.subList(listEnd, listSize));
            			
                        
                    } catch (Exception e) {
                        //记录出现异常的时间,线程name
                        result = new AsyncResult <String>("fail,time=" + System.currentTimeMillis() + ",thread id=" + Thread.currentThread().getName() + ",pageIndex=" + pageIndex);
                    }  
            }
            return result;
        }
    
    }

    创建线程分批传入Future:

    @Service
    public class ToolsThread {
    
    	private static final Logger LOG = LoggerFactory.getLogger(ToolsThread.class);
    
    	@Autowired
    	private SyncToolsHandler syncToolsHandler;
    
    	@Autowired
    	ReadToolsService readToolsService;
    
    	// 核心线程数
    	@Value("${book.core.poolsize}")
    	private int threadSum;
    
    	public void receiveBookJobRun() {
    
    		List<ReadToolModel> readToolModels = new ArrayList<ReadToolModel>();
    
    		readToolModels = readToolsService.getToolListByAutoId(0, 5000000);
    		// 入库开始时间
    		Long inserOrUpdateBegin = System.currentTimeMillis();
    
    		LOG.info("数据更新开始时间:" + inserOrUpdateBegin);
    
    		// 接收集合各段的 执行的返回结果
    		List<Future<String>> futureList = new ArrayList<Future<String>>();
    		// 集合总条数
    		if (readToolModels != null) {
    			// 将集合切分的段数(2*CPU的核心数)
    			int threadSum = 2 * Runtime.getRuntime().availableProcessors();
    			int listSize = readToolModels.size();
    			int listStart, listEnd;
    			// 当总条数不足threadSum条时 用总条数 当做线程切分值
    			if (threadSum > listSize) {
    				threadSum = listSize;
    			}
    
    			// 将list 切分多份 多线程执行
    			for (int i = 0; i < threadSum; i++) {
    				// 计算切割 开始和结束
    				listStart = listSize / threadSum * i;
    				listEnd = listSize / threadSum * (i + 1);
    				// 最后一段线程会 出现与其他线程不等的情况
    				if (i == threadSum - 1) {
    					listEnd = listSize;
    				}
    				// 数据切断
    				List<ReadToolModel> readToolList= readToolModels.subList(listStart, listEnd);
    
    				// 每段数据集合并行入库
    				futureList.add(syncToolsHandler.syncTools(readToolList, i));
    			}
    
    			// 对各个线程段结果进行解析
    			for (Future<String> future : futureList) {
    				String str;
    				if (null != future) {
    					try {
    						str = future.get().toString();
    						LOG.info("current thread id =" + Thread.currentThread().getName() + ",result=" + str);
    
    					} catch (ExecutionException | InterruptedException e) {
    
    						LOG.info("线程运行异常!");
    					}
    
    				} else {
    					LOG.info("线程运行异常!");
    				}
    			}
    		}
    
    		Long inserOrUpdateEnd = System.currentTimeMillis();
    		LOG.info("数据更新结束时间:" + inserOrUpdateEnd + "。此次更新数据花费时间为:" + (inserOrUpdateEnd - inserOrUpdateBegin));
    	}
    }

    同步时间大概15分钟

    2020-08-08 16:58:29 [main] INFO  com.commons.service.sync.ToolsThread -数据更新结束时间:1596877109637。此次更新数据花费时间为:990284

     

    展开全文
  • 要学习提升爬虫速度用到的知识,必须先熟悉并发和并行、同步和异步的概 一、并发和并行,同步和异步 并发和并行 并发(concurrency)和并行(parallelism)是两个相似的概念。并发是指在一个时间段内发生若干事件的...

    本系列为自己学习爬虫的相关笔记,如有误,欢迎大家指正

    要学习提升爬虫速度用到的知识,必须先熟悉并发和并行、同步和异步的概

    一、并发和并行,同步和异步

    并发和并行

    并发(concurrency)和并行(parallelism)是两个相似的概念。并发是指在一个时间段内发生若干事件的情况,并行是指在同一时刻发生若干事件的情况。

    使用单核CPU和多核CPU来说就是:在使用单核CPU时,多个工作任务是以并发的方式运行的,因为只有一个CPU,所以各个任务会分别占用CPU的一段时间依次执行。如果在自己分得的时间段没有完成任务,就会切换到另一个任务,然后在下一次得到CPU使用权的时候再继续执行,直到完成。在这种情况下,因为各个任务的时间段很短、经常切换,所以给我们的感觉是“同时”进行。在使用多核CPU时,在各个核的任务能够同时运行,这是真正的同时运行,也就是并行。

    同步和异步

    同步就是并发或并行的各个任务不是独自运行的,任务之间有一定的交替顺序,可能在运行完一个任务得到结果后,另一个任务才会开始运行。就像接力赛跑一样,要拿到交接棒之后下一个选手才可以开始跑。

    异步则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个任务影响,任务之间就像比赛的各个选手在不同的赛道比赛一样,跑步的速度不受其他赛道选手的影响。

    二、多线程爬虫

    多线程爬虫是以并发的方式执行的。也就是说,多个线程并不能真正的同时执行,而是通过进程的快速切换加快网络爬虫速度的。

    在Python设计之初,为了数据安全所做的决定设置有GIL(Global Interpreter Lock,全局解释器锁)。在Python中,一个线程的执行过程包括获取GIL、执行代码直到挂起和释放GIL。在一个Python进程中,只有一个GIL,拿不到GIL的线程就不允许进入CPU执行。

    正因为如此,在多核CPU上Python的多线程效率也不高。因为每次释放GIL锁,线程之间都会进行锁竞争,而切换线程会消耗资源。

    虽然如此,但是因为网络爬虫是IO密集型,线程能够有效地提升效率,因为单线程下有IO操作会进行IO等待,所以会造成不必要的时间浪费,而开启多线程能在线程A等待时自动切换到线程B,可以不浪费CPU的资源,从而提升程序执行的效率。

    Python的多线程对于IO密集型代码比较友好

    使用单线程爬虫

    import requests
    import time
    link_list = []
    
    with open('most.txt') as file:
        file_list = file.readlines()
        for each in file_list:
            link = each.split()[1]
            link = link.replace('\n','')
            link_list.append(link)
        start = time.time()
        for eachone in link_list:
            try:
                r = requests.get(eachone)
                print(r.status_code,eachone)
            except Exception as e:
                print('Error:',e)
        end = time.time()
        print('串行时间:',end-start)
    

    使用多线程

    在python中使用多线程有两种方法

    1.函数式

    调用_thread模块中的start_new_thread()函数产生新线程

    简单示例

    import _thread
    import time
    # 为线程定义一个函数
    def print_time(threadName,delay):
        count = 0
        while count < 3:
            time.sleep(delay)
            count += 1
            print(threadName,time.ctime())
    
    _thread.start_new_thread(print_time,('Thread-1',1))
    _thread.start_new_thread(print_time,('Thread-2',2))
    
    #time.sleep(5)
    print('Main Finished')
    

    这个代码没出现自己想要的结果,暂时没看出来问题在哪。

    • _thread中使用start_new_thread ()函数来产生新线程

      def start_new_thread(function, args, kwargs=None): 
      
    • function表示线程函数

    • args为传递给线程函数的参数,它必须是tuple类型

    2.类包装式

    调用Threading库创建线程,从threading.Thread继承

    threading模块提供了Thread类来处理线程,包括以下方法。

    • run():用以表示线程活动的方法。
    • start():启动线程活动。
    • join([time]):等待至线程中止。阻塞调用线程直至线程的join()方法被调用为止。
    • isAlive():返回线程是否是活动的。
    • getName():返回线程名。
    • setName():设置线程名。

    示例:

    import threading
    import  time
    
    class myThread(threading.Thread):
        def __init__(self,name,delay):
            threading.Thread.__init__(self)
            self.name = name
            self.delay = delay
        def run(self):
            print('Starting:'+self.name)
            print_time(self.name,self.delay)
            print('Exiting:'+self.name)
    def print_time(threadName,delay):
        counter = 0;
        while counter<3:
            time.sleep(delay)
            print(threadName,time.ctime())
            counter += 1
    
    threads = []
    # 创建新线程
    thread1 = myThread('Thread-1',1)
    thread2 = myThread('Thread-2',2)
    # 开启新线程
    thread1.start()
    thread2.start()
    
    # 添加线程到线程列表
    threads.append(thread1)
    threads.append(thread2)
    
    for t in threads:
        t.join()
    print('Exiting Main Thread')
    

    2.1简单的多线程爬虫

    import threading
    import requests
    import time
    
    link_list = []
    
    with open('most.txt','r') as file:
        file_list = file.readlines();
        for eachone in file_list:
            link = eachone.split()[1]
            link = link.replace('\n','')
            link_list.append(link)
    
    start = time.time()
    
    class myThread(threading.Thread):
        def __init__(self,name,link_range):
            threading.Thread.__init__(self)
            self.name = name
            self.link_range = link_range
    
        def run(self):
            print('starting '+self.name)
            crawler(self.name,self.link_range)
            print('exiting '+self.name)
    
    def crawler(threadName,link_range):
        for i in range(link_range[0],link_range[1]+1):
            try:
                r = requests.get(link_list[i],timeout=20)
                print(threadName,r.status_code,link_list[i])
            except Exception as e:
                print(threadName,'Error: ' ,e)
    
    link_range_list = [(0,200),(201,400),(401,600),(601,800),(801,1000)]
    thread_list = []
    
    # 创建新线程
    for i in range(1,6):
        thread = myThread('Thread-'+str(i),link_range_list[i-1])
        thread.start()
        thread_list.append(thread)
    
    # 等待所有线程完成
    for thread in thread_list: 
        thread.join() #thread.join()方法等待各个线程执行完毕
    
    end = time.time()
    
    print('简单多线程爬虫的总时间为:',end-start)
    print('Exiting Main Thread')
    

    这个代码还是有改进的余地,比如说某一个线程中的200个已经结束了,那么就还剩4个线程。到最后就可能变成单线程了。

    2.2 queue多线程爬虫

    import threading
    import  time
    import requests
    import queue as Queue
    
    
    link_list = []
    with open('most.txt','r') as file:
        file_list = file.readlines()
        for eachone in file_list:
            link = eachone.split()[1]
            link = link.replace('\n','')
            link_list.append(link)
    
    start = time.time()
    
    class myThread(threading.Thread):
        def __init__(self,threadName,q):
            threading.Thread.__init__(self)
            self.name = threadName
            self.q=q
        def run(self):
            print('starting '+self.name)
            while True:
                try:
                    crawler(self.name,self.q)
                except:
                    break
            print('exit '+self.name)
    
    def crawler(threadName,q):
        url = q.get(timeout=2)
        try:
            r = requests.get(url,timeout=20)
            print(q.qsize(),threadName,r.status_code,url)
        except Exception as e:
            print(q.qsize(), threadName, url, 'Error:',e)
    thread_list = ['Thread-1','Thread-2','Thread-3','Thread-4','Thread-5']
    workQueue = Queue.Queue(1000)
    threads = []
    # 创建新线程
    for tName in thread_list:
        thread = myThread(tName,workQueue)
        thread.start()
        threads.append(thread)
    #填充队列
    for url in link_list:
        workQueue.put(url)
    
    # 等待所有线程完成
    for t in threads:
        t.join()
    
    end = time.time()
    print('Queue多线程爬虫的总时间:'+end-start)
    print('Exiting Main Thread')
    
    

    Python的多线程爬虫只能运行在单核上,各个线程以并发的方法异步运行。由于GIL(Global Interpreter Lock,全局解释器锁)的存在,多线程爬虫并不能充分地发挥多核CPU的资源。

    三、多进程爬虫

    多进程爬虫则可以利用CPU的多核,进程数取决于计算机CPU的处理器个数。由于运行在不同的核上,各个进程的运行是并行的。在Python中,如果我们要用多进程,就需要用到multiprocessing这个库。

    使用multiprocess库的两种方法

    当进程数量大于CPU的内核数量时,等待运行的进程会等到其他进程运行完毕让出内核为止。因此,如果CPU是单核,就无法进行多进程并行。在使用多进程爬虫之前,我们需要先了解计算机CPU的核心数量

    from multiprocessing import cpu_count
    print(cpu_count())
    

    1.使用Process + Queue的多进程爬虫

    from multiprocessing import cpu_count
    from multiprocessing import Process,Queue
    import time
    import requests
    link_list = []
    print(cpu_count())
    
    with open('most.txt','r') as file:
        file_list = file.readlines()
        for eachfile in file_list:
            link = eachfile.split()[1]
            link = link.replace('\n','')
            link_list.append(link)
    
    start = time.time()
    class MyProcess(Process):
        def __init__(self,q):
            Process.__init__(self)
            self.q = q
    
        def run(self):
            print('starting:' ,self.pid)
            while not  self.q.empty():
                crawler(self.q)
            print('exit:',self.pid)
    
    
    
    def crawler(q):
        url = q.get(timeout = 2)
        try:
            r = requests.get(url,timeout=20)
            print(q.qsize(),r.status_code,url)
        except Exception as e:
            print(q.qsize(),url,'Error:',e)
    
    if __name__ =='__main__':
        ProcessNames = ['Process-1','Process-2','Process-3','Process-4','Process-5']
        workQueue = Queue(1000)
        # 填充队列
        for url in link_list:
            workQueue.put(url)
        for i in range(0,3):
            p = MyProcess(workQueue)
            p.daemon = True #如果将daemon设置为True,当父进程结束后,子进程就会自动被终止。
            p.start()
            p.join()
        end = time.time()
        print('Process + Queue多进程爬虫的总时间为:',end-start)
    
        print('Main process Ended!')
    

    2.使用Pool + Queue的多进程爬虫

    from multiprocessing import Pool,Manager
    import time
    import requests
    link_list = []
    with open('most.txt','r') as file:
        file_list = file.readlines()
        for each in file_list:
            link = each.split()[1].replace('\n','')
            link_list.append(link)
    
    start = time.time()
    def crawler(q,index):
        Process_id = 'Process-'+str(index)
        while not  q.empty():
            url = q.get(timeout=2)
            try:
                r = requests.get(url,timeout=20)
                print(Process_id,q.qsize(),r.status_code,url)
            except Exception as e:
                print(Process_id,q.qsize(),url,'Error:',e)
    
    if __name__ == '__main__':
        manager = Manager()
        workQueue = manager.Queue(1000)
        # 填充队列
        for url in link_list:
            workQueue.put(url)
        pool = Pool(processes=3) #使用Pool(processes=3)创建线程池的最大值为3
        for i in range(4):
            pool.apply_async(crawler,args=(workQueue,i)) # 创建子进程 这里采用的是非阻塞方法
        print('Start process')
        pool.close()
        pool.join()
        end = time.time()
        print('Pool +Queue多进程爬虫的总时间为:',end-start)
        print('Main process End')
    

    四、多协程(Coroutine)爬虫

    使用协程的好处:

    • 协程像一种在程序级别模拟系统级别的进程,由于是单线程,并且少了上下文切换,因此相对来说系统消耗很少
    • 协程方便切换控制流,这就简化了编程模型。协程能保留上一次调用时的状态(所有局部状态的一个特定组合),每次过程重入时,就相当于进入了上一次调用的状态。
    • 协程的高扩展性和高并发性,一个CPU支持上万协程都不是问题,所以很适合用于高并发处理。

    协程的缺点:

    • 协程的本质是一个单线程,不能同时使用单个CPU的多核,需要和进程配合才能运行在多CPU上
    • 有长时间阻塞的IO操作时不要用协程,因为可能会阻塞整个程序

    在python的协程中可以使用gevent库

    pip install gevent

    import gevent
    
    from gevent.queue import Queue,Empty
    import time
    
    #把下面有可能有IO操作的单独坐上标记
    """
    以下两行,可以实现爬虫的并发能力,如果没有这两句的话,整个抓取过程就会变成
    依次抓取gevent库中的monkey能把可能有IO操作的单独做上标记,将IO变成可以异步执行的函数
    """
    from gevent import monkey
    monkey.patch_all()#将IO转为异步执行的函数
    import requests
    link_list = []
    
    with open('most.txt','r') as file:
        file_list = file.readlines()
        for each in file_list:
            link_list.append(each.split()[1].replace('\n',''))
    
    start = time.time()
    
    def crawler(index):
        Process_id = 'Process-'+str(index)
        while not workQueue.empty():
            url = workQueue.get(timeout=2)
            try:
                r = requests.get(url,timeout=20)
                print(Process_id,workQueue.qsize(),r.status_code,url)
            except Exception as e:
                print(Process_id, workQueue.qsize(),  url,'Error',e)
    
    def boss():
        for url in link_list:
            workQueue.put_nowait(url)
    if __name__ =='__main__':
        workQueue = Queue(1000)
        gevent.spawn(boss).join() # 将队列中加入的内容整合到gevent中
        '''
        下面4行是创建多协程爬虫的程序
        '''
        jobs = []
        for i in range(10): #创建10个协程
            jobs.append(gevent.spawn(crawler,i))
        gevent.joinall(jobs)
        end = time.time()
        print('多协程爬虫的总时间为:',end-start)
        print('Main Ended')
    

    五、总结

    1. 并发(concurrency)和并行(parallelism):并发是指在一个时间段发生若干事件的情况。并行是指在同一时刻发生若干事件的情况

    2. 同步是指并发或并行的各个任务不是独自运行的,任务之间有一定的交替顺序,可能在执行完一个任务并得到结果后,另一个任务才会开始运行。

    3. 异步则是并发或并行的各个任务可以独立运行,一个任务的运行不受另一个影响。

    4. 多线程的方式:
      在这里插入图片描述

      程序的执行是在不同线程之间切换的。当一个线程等待网页下载时,进程可以切换到其他线程执行。

    5. 多进程的方式
      在这里插入图片描述

      程序的执行是并行、异步的,多个线程可以在同一时刻发生若干事件。

    6. 多协程的执行方式
      在这里插入图片描述

      协程是一种用户态的轻量级线程,在程序级别来模拟系统级别用的进程。在一个进程中,一个线程通过程序的模拟方法实现高并发。

      微信搜一搜【梓莘】或扫描下方二维码交个朋友共同进步。文章持续更新中。目前在整理爬虫相关学习笔记,期待后续更多的更新
      扫码关注公众号

    展开全文
  • 有序性编程要求题第2关:使用synchronized关键字同步线程任务描述相关知识并发编程什么时候会出现安全问题如何解决线程安全问题synchronized关键字synchronized代码块编程要求测试说明代码示例第3关:使用线程锁...

    第1关:并发编程的三个概念

    任务描述

    在我们进行应用开发的时候,常常会关注网站的并发,如果网站的用户量很多,当这些用户同时访问一个服务的时候,我们的服务器就会接收到大量的并发请求,处理好这些并发请求是一个合格程序员必须要完成的工作。

    理解并发编程的三个概念对于我们更好的开发高并发的Web应用有很大的帮助。

    本关的任务就是理解并发编程的三个重要概念并完成右侧选择题。

    相关知识

    1.原子性

    原子性:即一个操作或者多个操作,要么全部执行并且在执行过程中不会被任何因素打断,要么就不执行。

    我们来看看下面这段代码:

    	x = 10;        //语句1
    	y = x;         //语句2
    	x++;           //语句3
    	x = x + 1;   	  //语句4
    

    现在请你判断,这段代码哪些是原子操作。

    可能你会觉得四个语句都是原子操作,可是实际上只有语句1是原子性操作。 语句1是直接将10的值赋值给x,所以也就是说执行这个语句会直接将数值10写入到内存中,所以这是原子性的,语句2其实是两个操作,先读取x的值,然后赋值给y,这两个步骤是原子性的,但是他们合起来就不是原子性操作了,后面两个语句也是同样的道理。

    也就是说,只有简单的读取,赋值(必须是将数字赋值给某个变量,变量之间的赋值不是原子性操作),才是原子性操作。

    从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现大范围的原子性,可以通过synchronizedlock来实现,lock(锁)和synchronized(同步)在后面的关卡会介绍。

    synchronizedlock可以保证在任何时候只有一个线程执行该代码块,所以就保证了原子性。

    2.可见性

    可见性是当多个线程访问一个变量时,一个线程改变了变量的值,其他线程立马可以知道这个改变。

    举个例子:

    	//线程1执行的代码
    	int i = 0;
    	i = 10;
    	 
    	//线程2执行的代码
    	j = i;
    

    假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

    此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10

    这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

    对于可见性,Java提供了Volatile关键字来保证,当一个变量被Volatile修饰时,它会保证修改的值会被立即重新写入到主内存,当其他线程要调用该共享变量时,会去主内存中重新读取。

    但是普通的共享变量是不能保证可见性的,因为普通变量会被读入到线程自己的内存,当一个线程修改了之后,可能还没来得及刷新到主内存,其他线程就从主存中读取了该变量。所以其他线程读取的时候可能还是原来的值,所以普通共享变量是无法保证可见性的。

    关于保证可见性还可以通过Synchronizedlock的方式来实现。

    3.有序性

    有序性:即程序的执行是按照代码编写的先后顺序执行的,例如下面这个例子:

    	int a;              
    	boolean flag;
    	i = 10;                //语句1  
    	flag = false;         	 //语句2
    

    上述代码定义了一个整形的变量a,布尔类型变量flag,使用语句1和语句2对变量iflag进行赋值,看起来语句1是在语句2之前的,但是在程序运行的时候语句1一定会在语句2之前执行吗?是不一定的,因为这里可能会发生指令重排序Instruction Reorder)。

    什么是指令重排序呢?

    一般来说,处理器为了提升执行效率,会对输入代码进行优化,它不保证代码执行的顺序和代码编写的顺序一致,但是它会保证程序的输出结果和代码的顺序执行结果是一致的

    比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

    但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

    	int a = 3;        //语句1
    	int b = 5;        //语句2
    	a = a + 3;        //语句3
    	b = b + a + 4;   	 //语句4
    

    上述程序的执行结果就可能是:语句2 => 语句1 => 语句3 => 语句4

    那有没有可能是:语句2 => 语句1 => 语句4 => 语句3呢?

    不可能,因为处理器在执行语句的时候会考虑数据之间的依赖性,上述代码语句4是要依赖语句3的结果的,所以处理器会保证语句3在语句4之前执行。

    如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

    虽然指令重排序不会影响单个线程的最终执行结果,但是多线程的情况下会不会影响呢?我们来看一个例子:

    	//线程1:
    	context = loadContext();   //语句1
    	inited = true;             //语句2
    	 
    	//线程2:
    	while(!inited ){
    	  sleep();
    	}
    	doSomethingwithconfig(context);
    

    可以发现语句1和语句2并没有数据依赖性,所以按照指令重排序的规则,可能语句2在语句1之前执行,语句2执行完之后,语句1还没开始执行,可能线程2就开始执行了,这个时候initedtrue,会跳出while循环转而执行doSomethingwithconfig(context)而这个时候语句1还没执行,context还没有初始化,就会造成程序报错。

    从上述例子可以看出,指令重排序不会影响单个线程的执行,但是会影响多线程的执行。

    也就是说,要保证多线程程序执行的正确性,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    编程要求

    略 ####测试说明 略

    • 1、在并发编程中,我们需要以下哪几个特性来保持多线程程序执行正确( A

      A、可见性

      B、原子性

      C、并发性

      D、有序性

    • 2、请分析以下语句哪些是原子操作( AB

      A、int a = 3;

      B、boolean flag = false;

      C、a–;

      D、a =a *a

    • 3、以下代码的执行结果是(E

      public class Test {
          public  int inc = 0;
           
          public void increase() {
              inc++;
          }
           
          public static void main(String[] args) {
              final Test test = new Test();
              for(int i=0;i<10;i++){
                  new Thread(){
                      public void run() {
                          for(int j=0;j<1000;j++)
                              test.increase();
                      };
                  }.start();
              }
               
              while(Thread.activeCount()>1)  //保证前面的线程都执行完
                  Thread.yield();
              System.out.println(test.inc);
          }
      }
      

      A、10000

      B、9870

      C、大于10000

      D、小于10000

      E、不一定,大概率小于一万

    第2关:使用synchronized关键字同步线程

    任务描述

    本关任务:使右侧代码中的insert方法在同一时刻只有一个线程能访问。

    相关知识

    为了完成本关任务,你需要掌握:

    1.并发编程什么时候会出现安全问题;

    2.怎么解决线程安全问题;

    3.synchronized关键字。

    并发编程什么时候会出现安全问题

    在单线程的时候是不会出现安全问题的,不过在多线程的情况下就很有可能出现,比如说:多个线程同时访问同一个共享资源,多个线程同时向数据库插入数据,这些时候如果我们不做任何处理,就很有可能出现数据实际结果与我们预期的结果不符合的情况。

    img

    现在有两个线程同时获取用户输入的数据,然后将数据插入到同一张表中,要求不能出现重复的数据。

    我们必然要在插入数据的时候进行如下操作:

    • 检查数据库中是否存在该数据;
    • 如果存在则不插入,否则插入。

    现在有两个线程ThreadAThreadB来对数据库进行操作,当某个时刻,线程A和B同时读取到了数据X,这个时候他们都去数据库验证X是否存在,得到的结果都是不存在,然后A、B线程都向数据库插入了X数据,这个时候数据库中出现了两条X数据,还是出现了数据重复。

    这个就是线程安全问题,多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果

    这里面,这个资源被称为:临界资源(也可以叫共享资源)。

    当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等等)时,就有可能产生线程安全问题。

    当多个线程执行一个方法时,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。

    如何解决线程安全问题

    怎么解决线程的安全问题呢?

    基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称作同步互斥访问。

    在Java中一般采用synchronizedLock来实现同步互斥访问。

    synchronized关键字

    首先我们先来了解一下互斥锁,互斥锁:就是能达到互斥访问目的的锁。

    如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。

    在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。

    在我们编写代码的时候,可以使用synchronized修饰对象的方法或者代码块,当某个线程访问这个对象synchronized方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行该对象的方法。

    我们来看个示例进一步理解synchronized关键字:

    public class Example {
     
        public static void main(String[] args)  {
            final InsertData insertData = new InsertData();
            
            new Thread() {
                public void run() {
                    insertData.insert(Thread.currentThread());
                };
            }.start();
            new Thread() {
                public void run() {
                    insertData.insert(Thread.currentThread());
                };
            }.start();
        }  
    }
    class InsertData {
        private ArrayList<Integer> arrayList = new ArrayList<Integer>();
         
        public void insert(Thread thread){
            for(int i=0;i<5;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
    

    这段代码的执行是随机的(每次结果都不一样):

    Thread-0在插入数据0

    Thread-1在插入数据0

    Thread-1在插入数据1

    Thread-1在插入数据2

    Thread-1在插入数据3

    Thread-1在插入数据4

    Thread-0在插入数据1

    Thread-0在插入数据2

    Thread-0在插入数据3

    Thread-0在插入数据4

    现在我们加上synchronized关键字来看看执行结果:

    public synchronized void insert(Thread thread){
         for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入数据"+i);
            arrayList.add(i);
        }
    }
    

    输出:

    Thread-0在插入数据0

    Thread-0在插入数据1

    Thread-0在插入数据2

    Thread-0在插入数据3

    Thread-0在插入数据4

    Thread-1在插入数据0

    Thread-1在插入数据1

    Thread-1在插入数据2

    Thread-1在插入数据3

    Thread-1在插入数据4

    可以发现,线程1会等待线程0插入完数据之后再执行,说明线程0和线程1是顺序执行的。

    从这两个示例中,我们可以知道synchronized关键字可以实现方法同步互斥访问。

    在使用synchronized关键字的时候有几个问题需要我们注意:

    1. 在线程调用synchronized的方法时,其他synchronized的方法是不能被访问的,道理很简单,一个对象只有一把锁;
    2. 当一个线程在访问对象的synchronized方法时,其他线程可以访问该对象的非synchronized方法,因为访问非synchronized不需要获取锁,是可以随意访问的;
    3. 如果一个线程A需要访问对象object1synchronized方法fun1,另外一个线程B需要访问对象object2synchronized方法fun1,即使object1object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

    synchronized代码块

    synchronized代码块对于我们优化多线程的代码很有帮助,首先我们来看看它长啥样:

    synchronized(synObject) {
        
    }
    

    当在某个线程中执行该段代码时,该线程会获取到该对象的synObject锁,此时其他线程无法访问这段代码块,synchronized的值可以是this代表当前对象,也可以是对象的属性,用对象的属性时,表示的是对象属性的锁。

    有了synchronized代码块,我们可以将上述添加数据的例子修改成如下两种形式:

    class InsertData {
        private ArrayList<Integer> arrayList = new ArrayList<Integer>();
         
        public void insert(Thread thread){
            synchronized (this) {
                for(int i=0;i<100;i++){
                    System.out.println(thread.getName()+"在插入数据"+i);
                    arrayList.add(i);
                }
            }
        }
    }
    
    class InsertData {
        private ArrayList<Integer> arrayList = new ArrayList<Integer>();
        private Object object = new Object();
         
        public void insert(Thread thread){
            synchronized (object) {
                for(int i=0;i<100;i++){
                    System.out.println(thread.getName()+"在插入数据"+i);
                    arrayList.add(i);
                }
            }
        }
    }
    

    上述代码就是synchronized代码块添加锁的两种方式,可以发现添加synchronized代码块,要比直接在方法上添加synchronized关键字更加灵活。

    当我们用sychronized关键字修饰方法时,这个方法只能同时让一个线程访问,但是有时候很可能只有一部分代码需要同步,而这个时候使用sychronized关键字修饰的方法是做不到的,但是使用sychronized代码块就可以实现这个功能。

    并且如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

    来看一段代码:

    public class Test {
     
        public static void main(String[] args)  {
            final InsertData insertData = new InsertData();
            new Thread(){
                public void run() {
                    insertData.insert();
                }
            }.start(); 
            new Thread(){
                public void run() {
                    insertData.insert1();
                }
            }.start();
        }  
    }
     
    class InsertData { 
        public synchronized void insert(){
            System.out.println("执行insert");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackT\frace();
            }
            System.out.println("执行insert完毕");
        }
         
        public synchronized static void insert1() {
            System.out.println("执行insert1");
            System.out.println("执行insert1完毕");
        }
    }
    

    执行结果:

    执行insert

    执行insert1

    执行insert1完毕

    执行insert完毕

    编程要求

    请仔细阅读右侧代码,根据方法内的提示,在Begin - End区域内进行代码补充,具体任务如下:

    • 使num变量在同一时刻只能有一个线程可以访问。

    测试说明

    使程序的输出结果如下:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18


    开始你的任务吧,祝你成功!

    代码示例

    package step2;
    
    public class Task {
    
    	public static void main(String[] args) {
    
    		final insertData insert = new insertData();
    
    		for (int i = 0; i < 3; i++) {
    			new Thread(new Runnable() {
    				public void run() {
    					insert.insert(Thread.currentThread());
    				}
    			}).start();
    		}
    
    	}
    }
    
    class insertData {
    
    	public static int num = 0;
    
    	/********* Begin *********/
    	public synchronized void insert(Thread thread) {
    
    		for (int i = 0; i <= 5; i++) {
    			num++;
    			System.out.println(num);
    		}
    	}
    
    	/********* End *********/
    }
    
    

    第3关:使用线程锁(Lock)实现线程同步

    任务描述2

    本关任务:使用Lock,实现对于某一块代码的互斥访问。

    相关知识

    上一关我们谈到了synchronized关键字,synchronized关键字主要用来同步代码,实现同步互斥访问,也就是在同一时刻只能有一个线程访问临界资源。从而解决线程的安全问题。

    如果一个方法或者代码块被synchronized关键字修饰,当线程获取到该方法或代码块的锁,其他线程是不能继续访问该方法或代码块的。

    而其他线程要能访问该方法或代码块,就必须要等待获取到锁的线程释放这个锁,而在这里释放锁只有两种情况:

    1. 线程执行完代码块,自动释放锁;
    2. 程序报错,jvm让线程自动释放锁。

    可能会有一种情况,当一个线程获取到对象的锁,然后在执行过程中因为一些原因(等待IO,调用sleep方法)被阻塞了,这个时候锁还在被阻塞的线程手中,而其他线程这个时候除了等之外,没有任何办法,我们想一想这样子会有多影响程序的效率。

    synchronized是Java提供的关键字,使用起来非常方便,不过在有些情况下,它是具有很多局限性的。

    因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

    在比如,当多个线程操作同一个文件的时候,同时读写是会冲突的,同时写也是会冲突的,但是同时读是不会发生冲突的,而我们如果用synchronized来实现同步,就会出现一个问题:

    如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

    因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,而通过Lock就可以办到。

    总的来说Lock要比synchronized提供的功能更多,可定制化的程度也更高,Lock不是Java语言内置的,而是一个类。

    Lock接口

    我们来了解一下反复提到的Lock,首先我们来查看它的源码:

    public interface Lock {
        void lock();
        void lockInterruptibly() throws InterruptedException;
        boolean tryLock();
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
        void unlock();
        Condition newCondition();
    }
    

    可以发现Lock是一个接口,其中:lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()方法是用来获取锁的,unlock()方法是用来释放锁的。

    首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

    由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

    一个使用Lock的例子:

    Lock lock = ...;
    lock.lock();
    try{
        //处理任务
    }catch(Exception ex){
         
    }finally{
        lock.unlock();   //释放锁
    }
    

    tryLock()顾名思义,是用来尝试获取锁的,并且该方法有返回值,表示获取成功与否,获取成功返回true,失败返回false,从方法可以发现,该方法如果没有获取到锁时不会继续等待的,而是会直接返回值。

    tryLock()的重载方法tryLock(long time, TimeUnit unit)功能类似,只是这个方法会等待一段时间获取锁,如果过了等待时间还未获取到锁就会返回false,如果在等待时间之内拿到锁则返回true

    所以我们经常这样使用:

    Lock lock = ...;
    if(lock.tryLock()) {
         try{
             //处理任务
         }catch(Exception ex){
             
         }finally{
             lock.unlock();   //释放锁
         } 
    }else {
        //如果不能获取锁,则直接做其他事情
    }
    

    lock()方法的正确使用

    因为Lock是一个接口所以我们在编程时一般会使用它的实现类,ReentrantLockLock接口的一个实现类,意思是“可重入锁”,接下来我们通过一个例子来学习lock()方法的正确使用方式。

    示例1:

    public class Test {
        private ArrayList<Integer> arrayList = new ArrayList<Integer>();
        public static void main(String[] args)  {
            final Test test = new Test();
             
            new Thread(){
                public void run() {
                    test.insert(Thread.currentThread());
                };
            }.start();
             
            new Thread(){
                public void run() {
                    test.insert(Thread.currentThread());
                };
            }.start();
        }  
         
        public void insert(Thread thread) {
            Lock lock = new ReentrantLock();    //注意这个地方
            lock.lock();
            try {
                System.out.println(thread.getName()+"得到了锁");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        }
    }
    

    输出:

    Thread-1得到了锁

    Thread-0得到了锁

    Thread-0释放了锁

    Thread-1释放了锁

    结果可能出乎你的意料,不对呀,按道理应该是一个线程得到锁其他线程不能获取锁了的啊,为什么会这样呢?是因为insert()方法中lock变量是一个局部变量。THread-0Thread-1获取到的是不同的锁,这样不会造成线程的等待。

    那怎么才能利用lock()实现同步呢?相信你已经想到了,只要将Lock定义成全局变量就可以了。

    public class Test {
        private ArrayList<Integer> arrayList = new ArrayList<Integer>();
        private Lock lock = new ReentrantLock();    //注意这个地方
        
        public static void main(String[] args)  {
            final Test test = new Test();
             
            new Thread(){
                public void run() {
                    test.insert(Thread.currentThread());
                };
            }.start();
             
            new Thread(){
                public void run() {
                    test.insert(Thread.currentThread());
                };
            }.start();
        }  
         
        public void insert(Thread thread) {
            lock.lock();
            try {
                System.out.println(thread.getName()+"得到了锁");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        }
    }
    

    结果:

    Thread-0得到了锁

    Thread-0释放了锁

    Thread-1得到了锁

    Thread-1释放了锁

    这样就是我们预期的结果了。

    很多时候我们为了提高程序的效率不希望线程为了等待锁而一直阻塞,这个时候可以使用tryLock()可以达到目的。

    示例,将之前的insert()方法修改成tryLock()实现:

     public void insert(Thread thread) {
            if(lock.tryLock()) {
                try {
                    System.out.println(thread.getName()+"得到了锁");
                    for(int i=0;i<5;i++) {
                        arrayList.add(i);
                    }
                } catch (Exception e) {
                    // TODO: handle exception
                }finally {
                    System.out.println(thread.getName()+"释放了锁");
                    lock.unlock();
                }
            } else {
                System.out.println(thread.getName()+"获取锁失败");
            }
    }
    

    输出:

    Thread-0得到了锁

    Thread-1获取锁失败

    Thread-0释放了锁

    编程要求

    请仔细阅读右侧代码,根据方法内的提示,在Begin - End区域内进行代码补充。 ####测试说明

    使得程序输出如下结果(因为线程的执行顺序是随机的可能需要你评测多次):

    Thread-0得到了锁

    1

    2

    3

    4

    5

    Thread-0释放了锁

    Thread-1得到了锁

    6

    7

    8

    9

    10

    Thread-1释放了锁

    Thread-2得到了锁

    11

    12

    13

    14

    15

    Thread-2释放了锁


    开始你的任务吧,祝你成功!

    代码示例

    package step3;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Task {
    
    	public static void main(String[] args) {
    		final Insert insert = new Insert();
    		Thread t1 = new Thread(new Runnable() {
    			public void run() {
    				insert.insert(Thread.currentThread());
    			}
    		});
    
    		Thread t2 = new Thread(new Runnable() {
    			public void run() {
    				insert.insert(Thread.currentThread());
    			}
    		});
    
    		Thread t3 = new Thread(new Runnable() {
    			public void run() {
    				insert.insert(Thread.currentThread());
    			}
    		});
    		// 设置线程优先级
    		t1.setPriority(Thread.MAX_PRIORITY);
    		t2.setPriority(Thread.NORM_PRIORITY);
    		t3.setPriority(Thread.MIN_PRIORITY);
    
    		t1.start();
    		t2.start();
    		t3.start();
    
    	}
    }
    
    class Insert {
    
    	public static int num;
    
    	// 在这里定义Lock
    	private ReentrantLock lock = new ReentrantLock();
    
    	public void insert(Thread thread) {
    		/********* Begin *********/
    
    		try {
    			lock.lock();
    			System.out.println(Thread.currentThread().getName() + "得到了锁");
    			for (int i = 0; i < 5; i++) {
    				num++;
    				System.out.println(num);
    			}
    
    		} finally {
    			lock.unlock();
    			System.out.println(Thread.currentThread().getName() + "释放了锁");
    		}
    
    		/********* End *********/
    	}
    
    }
    
    

    第4关:使用volatile实现变量的可见性

    任务描述

    本关任务:使用volatile关键字与同步实现右侧程序输出10000

    相关知识

    在并发编程中,volatile关键字扮演着非常重要的作用,接下来我们直接进入主题。

    什么是 volatile 关键字

    volatile是干啥用的,有什么含义和特点呢?

    1. 当一个共享变量被volatile修饰时,它就具备了“可见性”,即这个变量被一个线程修改时,这个改变会立即被其他线程知道。
    2. 当一个共享变量被volatile修饰时,会禁止“指令重排序”。

    先来看个例子:

    //线程1
    boolean stop = false;
    while(!stop){
        doSomething();
    }
     
    //线程2
    stop = true;
    

    因为线程是不直接提供停止的方法的,所以我们很多时候想要中断线程一般都会采用上述代码。

    不过这段代码是存在问题的,当线程2执行时,这段代码能保证一定能中断线程1吗?在大多数情况下是可以的,不过也有可能不能中断线程1。

    为何有可能导致无法中断线程呢?每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

    那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

    如何避免这种情况呢?

    很简单,给stop变量加上volatile关键字就可以了。

    volatile关键字会产生什么效果呢?

    1. 使用volatile关键字会强制将变量的修改的值立即写至主内存;
    2. 使用volatile关键字,当线程2对变量stop修改时,会强制将所有用到stop变量的线程对应的缓存中stop的缓存行置为无效。
    3. 由于线程1的stop缓存行无效,所以在运行时线程1会读取主存中stop变量的值。

    所以到最后线程1读取到的就是stop最新的值。

    volatile可以保证原子性吗

    在之前我们了解到了线程的三大特性:原子性,可见性,有序性。

    前面的例子我们知道了volatile可以保证共享变量的可见性,但是volatile可以保证原子性吗?

    我们来看看:

    public class Test {
        public volatile int inc = 0;
         
        public void increase() {
            inc++;
        }
         
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
             
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }
    

    来想一想这段程序的输出结果,然后copy到本地运行看一看效果。

    可能我们想的结果应该是:10000,不过最终运行的结果往往达不到10000,可能我们会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000

    前面我们讲到volatile是可以保证可见性的,不过上述程序的错误在于volatile没法保证程序的原子性。

    我们知道变量的自增不是原子性的。它包括两个步骤:

    1.读取变量的值;

    2.给变量的值加1并写入工作内存。

    我们想象这样一种情况:线程1在操作inc变量自增的时候可能会遇到这种状况,读取到了inc变量的值,这个时候inc的值为10,还没有进行自增操作时候线程1阻塞了,紧接着线程2对inc变量进行操作,注意这个时候inc的值还是10,线程2对inc进行了自增操作,这个时候inc的值是11,并将这个改变写到主存中,好了,现在线程1恢复了,它并不会去主存中读取inc的值,因为inc已经在它的缓存中了,所以继续进行之前的操作,注意这个时候线程1的缓存中inc的值是10,线程1对inc的值进行加1.inc等于11,然后写入主存。

    我们发现两个线程都对inc进行了一轮操作,但是inc的值只增加了1.

    可能我们还是会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

    理解了这些我们就能明白,这个问题的根源就在于自增操作不是原子性的。

    而要解决这个问题就很简单了,让自增操作变成原子性就可以了。

    怎么保证原子性呢,怎么让上述代码结果是10000呢?

    用我们前两关学习的知识就可以啦,具体代码自己思考吧,毕竟从自己脑子里想出来的才是自己的呢。

    编程要求

    请仔细阅读右侧代码,根据方法内的提示,在Begin - End区域内进行代码补充。 ####测试说明

    预期输出:10000

    提示:可以使用两种方式实现原子性,所以本关有多种方式都可以通关。


    开始你的任务吧,祝你成功!

    代码示例

    package step4;
    
    public class Task {
    	public volatile int inc = 0;
    
    //请在此添加实现代码
    	/********** Begin **********/
    	public synchronized void increase() {
    
    		inc++;
    
    	}
    
    	/********** End **********/
    
    	public static void main(String[] args) {
    		final Task test = new Task();
    		for (int i = 0; i < 10; i++) {
    			new Thread() {
    				public void run() {
    					for (int j = 0; j < 1000; j++)
    						test.increase();
    				};
    			}.start();
    		}
    		while (Thread.activeCount() > 1) // 保证前面的线程都执行完
    			Thread.yield();
    		System.out.println(test.inc);
    	}
    }
    
    
    展开全文
  • 同步代码块(SellTicket线程)3.解决办法——同步函数2.同步函数(SellTicket线程)总结 一、线程安全与互斥(案例说明) 1.经典案例 1、有100张电影票 2、设置三个窗口同时卖电影票 3、观察卖电影票的过程 2.代码...
  • Java 多线程同步和异步详解

    千次阅读 2018-05-31 10:00:32
    转载自 https://www.cnblogs.com/mengyuxin/p/5358364.htmljava线程 同步与异步 线程池1)多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线程的处理的数据,而B线程又修改了...
  • 描述:由C#编写的多线程异步抓取网页的网络爬虫控制台程序 ... 之所以用多线程异步抓取完全是出于效率考虑,本程序多线程同步并不能带来速度的提升,只要抓取的网页不要太多重复和冗余就可以,异步并不意味着错误。
  • C++多线程编程实战

    2018-06-04 11:03:11
    你将学会如何从多线程方案中受益,提升自己的开发能力,构建更好的应用程序。本书不仅讲解了创建并行代码时会遇到的问题,而且还帮助读者更好地理解同步技术。本书的目标是帮助读者在理解多线程编程概念的同时,能...
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    史上最强多线程面试47题(含答案),建议收藏 金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素?...
  • Redis 6.0多线程模型总结

    千次阅读 2022-01-24 00:06:10
    这个版本提供了诸多令人心动的新特性及功能改进,比如新网络协议RESP3,新的集群代理,ACL等,其中关注度比较高的应该是多线程模型了。 1、Redis6.0之前的版本真的是单线程吗? Redis在处理客户端的请求时,包括...
  • 爱生活,爱编码,微信搜一搜...1、其实是否应该使用多线程在很程度上取决于应用程序的类型。 计算密集型(如纯数学运算) 的, 并受CPU 功能的制约, 则只有多CPU(或者多个内核) 机器能够从更多的线程中受益, 单C.
  • Java多线程学习(吐血超详细总结)

    万次阅读 多人点赞 2015-03-14 13:13:17
    本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。
  • 前几篇:Java多线程编程-(1)-线程安全和锁Synchronized概念Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性Java多线程编程-(3)-线程本地ThreadLocal的介绍与使用Java多线程编程-(4)-线程间通信...
  • 同步多线程

    千次阅读 2013-11-02 08:50:57
    同步多线程(SMT)是一种在一个CPU 的时钟周期内能够执行来自多个线程的指令的硬件多线程技术。本质上,同步多线程是一种将线程级并行处理(多CPU)转化为指令级并行处理(同一CPU)的方法。 同步多线程是单个物理...
  • 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程提升整体处理性能。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同制的线程来执行不同的任务,允许...
  • 定义:多线程读写共享变量时出现不正确的行为 原因 原子性问题 CPU时钟中断带来的线程切换 可见性问题 多核CPU高速缓存之间不可见 重排序问题 CPU和编译器会进行重排序指令 典型问题:单例模式DCL ...
  • OpenCV视频流的C++多线程处理方式

    千次阅读 2021-01-14 11:20:08
    目录为什么需要多线程处理视频流C++的多线程处理方式函数封装的实现方式类封装的实现方式可能遇到的问题 为什么需要多线程处理视频流 在之前有写过一篇文章Python环境下OpenCV视频流的多线程处理方式,上面简单记录...
  • 多线程爬虫(提升爬虫的速度)

    千次阅读 2017-11-17 10:50:43
    了解同步了异步的概念(操作系统)7.2多线程爬虫GIL(全局资源解释器),python属于 脚本语言,通过解释器运行,区别的编译语言。爬虫属于,本机和服务器的io操作7.2.2学习python多线程 (1)函数式:调用_thread...
  • 多进程和多线程的主要区别是:线程是进程的子集(部分),一个进程可能由多个线程组成。多进程的数据是分开的、共享复杂,需要用IPC;但同步简单。多线程共享进程数据,共享简单;但同步复杂。  多进程,Windows...
  • Java多线程面试题(面试必备)

    万次阅读 多人点赞 2020-05-26 01:15:38
    文章目录一、多线程基础基础知识1. 并发编程1.1 并发编程的优缺点1.2 并发编程的三要素1.3 并发和并行有和区别1.4 什么是多线程多线程的优劣?2. 线程与进程2.1 什么是线程与进程2.2 线程与进程的区别2.3 用户线程...
  • Java 多线程编程基础(详细)

    万次阅读 多人点赞 2020-11-03 17:36:30
    Java多线程编程基础进程与线程多线程实现Thread类实现多线程Runnable接口实现多线程Callable接口实现多线程多线程运行状态多线程常用操作方法线程的命名和获取线程休眠线程中断线程强制执行线程让步线程优先级设定...
  • 金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 各位可以看看这些题自己能答上多少了,在这里我整理了2020大厂...
  • Java并发(一):多线程干货总结

    千次阅读 2021-02-25 18:45:33
    一、进程 线程进程:一个进程来对应一个程序,每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。当进程暂停时...
  • C++多线程并发(一)--- 线程创建与管理

    万次阅读 多人点赞 2020-03-16 22:21:32
    简单来说,并发指的是两个或个独立的活动在同一时段内交替发生。与并发相近的另一个概念是并行,并行则强调的是个独立的活动在同一时刻点同时发生。 二、为什么使用并发 在应用程序中使用并发的原因主要有两个:...
  • 多线程常见的面试题

    千次阅读 2020-09-16 16:21:58
    多线程常见的面试题: 1. 什么是线程和进程? 线程与进程的关系,区别及优缺点? 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 ...
  • Java多线程(一文看懂!)

    千次阅读 2021-07-28 17:36:15
    三,多线程的五状态 四,多线程的调度 五,线程的同步(多口售票问题) 六,线程的协作(生产者-消费者模型) 一,多线程的介绍 百度中多线程的介绍(multithreading):是指从软件或者硬件上实现多个线程并发...
  • 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程提升整体处理性能。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同制的线程来执行不同的任务,允许单...
  • 系统接受实现多用户多请求的高并发时,通过多线程来实现。 二、线程后台处理任务 一个程序是线性执行的。如果程序执行到要花大量时间处理的任务时,那主程序就得等待其执行完才能继续执行下面的。那用户就不得...
  • python多线程教程:python多线程详解

    千次阅读 2020-02-03 11:49:20
    文章目录一、线程介绍二、线程实现threading模块自定义线程守护线程主线程等待子线程结束多线程共享全局变量递归锁事件(Event类)三、GIL(Global Interpreter Lock)全局解释器锁 python多线程详解 一、线程介绍 ...
  • Java多线程面试题

    万次阅读 多人点赞 2020-10-25 15:56:40
    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话); wait 方法: 是 Object 的方法...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 159,122
精华内容 63,648
关键字:

同步多线程提升多大